CS 152: Lab 9

Title image Project 9
Spring 2020

Lab Exercise 9: Dictionaries and Inheritance

This project continues our work with classes within the domain of physical simulations. This is the third part of a multi-part project where we will look at increasingly complex physical simulations. This week we add more sophisticated collisions with more types of objects and make use of dictionaries in a small way to make our code more efficient.


Tasks

Setup

Make yourself a working directory for project 8. You will need the Zelle graphics package file graphicsPlus.py. The Zelle documentation, is also available.


  1. Dictionaries

    Video: Dictionaries

    Dictionaries are a different method of storing information

    A dictionary is like a list, but the information in a dictionary is not stored in sequential order with integer indexes. Instead, each value stored in a dictionary has a unique key for an index.

    In a dictionary, the key plays the same role as the index in a list. The difference is that a key can be any Python type that is immutable (can't be modified). These include the types int, str, float, and tuple. Examples of valid keys are: 1, 42, '42', 'ice cream', 'chocolate', 'cookie', 1.7, and (5, 'bluberry', 'muffins').

    Download the file direction.py. This is a simple graphical program that lets the user control the direction of a ball, changing the color of the ball depending on the direction of travel.

    There are two actions in the program that would normally call for an extensive if/elif/elif/else type of structure. The first is changing the direction based on the key press. The second is changing the color based on the direction. We're going to use dictionaries to eliminate the need for the conditional structure by mapping keys directly to directions of motion.

    1. First, create a dictionary called deltas at Step 1. Write out the full dictionary using the dictionary syntax. A dictionary is defined by curly brackets { }, and each entry in the dictionary has a key-value pair separated by a colon : . For example, the following would create a dictionary with one entry that has the key 'Left' and the value (-1, 0).
      d = { 'Left' : (-1, 0) }

      The dictionary you assign to deltas should have entries for the keys: 'Left', 'Right', 'Up', 'Down', 'space', 'r'. The values associated with those keys should be: (-1, 0), (1, 0), (0, -1), (0, 1), (0, 0), (0, -5), respectively.

    2. Second, create an empty dictionary called colors at Step 2. Build up this dictionary using multiple assignment statements. For example, the following would create an empty dictionary d and then assign the value 'red' to the key (-1, 0) and the value 'green' to the key (1, 0). (Yes, you can use a tuple as a key.)
      d = {}
      d[ (-1, 0) ] = 'red'
      d[ (1, 0) ] = 'green'

      Create entries in color for the keys (-1, 0), (1, 0), (0, -1), (0, 1), (0, 0). Give them the values: 'red', 'green', 'yellow', 'blue', 'grey', respectively.

    3. Third, inside the main loop, we need to check if the user hit a key to which we should respond. To do this, we need to know if the keypress in the key variable is one of the keys in the deltas dictionary. Python allows us to test if a key is in a dictionary using the keyword in, as in the following example, which tests if the string 'Left' is in the dictionary d.
      if 'Left' in d:

      What we want to know is whether the string in the variable key is in the dictionary deltas. Set up that if statement.

    4. Fourth, we want to set the ball's direction according to the key. Normally we would have to create an if/elif/else type of structure to determine what to do. However, our dictionary associates each key with its corresponding direction of motion. If the key the user pressed is in the dictionary, we can use it to directly convert the key press into the movement direction associated with that key.

      Given a key that is in a dictionary, we can always access its value using the standard index notation. The following, for example, assigns to the variable thing the value in dictionary d associated with the string 'Left'.

      thing = d['Left']

      Assign to the variable direction the result of indexing into the dicationary deltas using the value in the variable key.

      Put a print statement right after the assignment and print out the value of direction.

    5. Fifth, we want to set the ball's color to the value associated with the direction. Again, we could use a big if/elif/else statement. However, we have a dictionary that associates a direction with a color string, so we can lookup the color in a single statement.

      Since there are more possible directions than we have entries in the dictionary, we aren't sure that the current value of direction is a valid key. One way to get around this is to test if direction is in the dictionary. However, we can also use the dictionary get method, which lets us specify a default return value if the key is not in the dictionary. The following code shows the syntax for using the get method of dictionary d to lookup the key (20, 10) and return the value 'purple' if that is not a valid key.

      a_value = d.get( (20, 10), 'purple' )

      Assign to a variable (e.g. newcolor) the result of using the get method of the colors dictionary with the variable direction as the key. Use a color string of your choice as the default return value (just make it different from the earlier colors, like 'purple').

      Print out the newcolor variable and then use it as the argument to the ball's setFill method.

    Test out your program and try controlling the ball with the arrow keys, the space bar, and the 'r' key. Use the 'q' key to quit.


  2. Inheritance

    Video Inheritance

    Inheritance is sharing the capabilities of one class with one or more other classes.

    This week we are going to use inheritance to simplify the code in the physics_objects.py file and make it easier to create new kinds of shapes. The concept is to create a parent class, Thing, that has all of the common fields and methods for physical objects. Then we create child classes, like Ball, Block, and Triangle, that make use of the parent Thing class.

    1. Define a parent Thing class

      Create a new file, physics_objects.py, in your project 9 directory. You may want to have a copy of your physics_objects.py file from last week open, since you will be able to re-use a good bit of the code.

      Start a new class, Thing. Give it a comment that this is the parent class for simulated objects.

    2. Define the Thing __init__ method

      The __init__ method for Thing should have two required parameters, win and the_type. The win parameter will be the GraphWin window for drawing and the_type will be the type of thing being created (e.g. a string like 'ball' or 'block').

      You can add any number of default parameters for the remaining fields. I would suggest having values for position, velocity, acceleration, color, and elasticity.

      In the __init__ method, create the following fields and assign them either a reasonable default value or their corresponding parameter. You don't have to use these specific field names.

      typeA string, indicating the type of the object.
      positionA 2-element list indicating the current position of the object.
      velocityA 2-element list indicating the current velocity of the object.
      accelerationA 2-element list indicating acceleration acting on the object.
      elasticityThe amount of energy retained after a collision.
      winA reference to the GraphWin object representing the window.
      shapesInitially an empty list that will hold Zelle graphics.
      drawnAn boolean indicating if the shape has been drawn, initially False.
      colorAn (r, g, b) tuple. A good default value is black (0, 0, 0).

      The Thing class constructor should not call the render method.

    3. Create the get methods

      Create get methods for the fields: type, position, velocity, acceleration, elasticity, and color. For the most of them, you can copy and paste these from the Ball class from last week. These methods have no parameters other than self. When returning a list (i.e. position, velocity, accleration) be sure to return a copy of the 2-element list.

    4. Create draw and undraw methods

      The draw method should loop over the self.shapes list of graphics objects and draw them into the window specified by self.win. The draw method should finish by setting self.drawn to True.

      The undraw method should also loop over the self.shapes list of graphics objects and undraw each one. The undraw method should finish by setting self.drawn to False.

      The draw and undraw methods have no parameters other than self. They should be identical to last week.

    5. Create set methods

      Create set methods for the fields position, velocity, acceleration, and elasticity. Make sure you are assigning a copy of the input data in the case of position, velocity, and acceleration, which should each take a 2-element list as the argument.

      The setPosition method needs to move the Zelle graphics elements, just as in prior weeks.

    6. Create the setColor method

      def setColor(self, c): # takes in a string or Zelle color object

      The setColor method should first set the color field to c. Then it should loop over self.shapes and set the fill color of each object to the specified color. You will probably want to override this method in the child classes.

    7. Create an update method

      The update method is identical to the prior week's method from the Ball class. It should update the simulated position and velocity using the standard equations of motion. It should also move the graphics objects in the shapes list by the appropriate amount.

  3. Create a new Ball class that inherits the Thing class

    class Ball(Thing):

    The Ball class will need four methods, __init__, render, getRadius, and setRadius. Except for the __init__ method, these will be identical to last week. You will also want to write a setColor function in the Ball class if you don't want all the elements that make up your ball to be the same color (default action in Thing class).

    1. Define the __init__ method

      The __init__ method should have win and radius as the only required arguments. You may also want to add optional arguments for position, velocity, acceleration, color, and elasticity with reasonable default values. The default value for elasticity should be 1.0 or something slightly smaller than 1 like 0.95.

      The first step in the __init__ method is to call the Thing (parent) __init__ method. Call it by using the name of the class, Thing, followed by the name of the method, __init__, connected by a dot. The first three arguments should be self, win, and the string "ball". Pass in any other optional parameters that match between the Ball __init__ and the Thing __init__ methods.

      Assign the radius to a field of self.

      Call self.render() to define the shapes list.

    2. Define a render method

      The render method is identical to last week. First, store the current drawn state of the ball. If the ball is drawn, it should undraw the ball. Then it should create the graphics objects for visualization and put them in the self.shapes list (the list should be completely rebuilt each time render is called). Finally, if the ball was drawn, it should redraw the graphics objects.

    3. Define the getRadius and setRadius methods.

      These should be the same as last week. The getRadius method should return the current radius. The setRadius method should assign the new value to the radius field and then call the render method.

    4. Define a setColor method (optional)

      If you want to do something besides set all of the graphics objects making up the ball to the same color, write a setColor method that does what you want it to do. For example, it might change the color of only the first element in the self.shapes list instead of all of them.

Once you are finished, all of the test functions from project 8 should work exactly the same way they did. Test your code and make sure everything is working properly. Then look how much code you were able to remove from your Ball class and put into the Thing class.

Test functions

When you are done with the lab exercises, you may begin the project.