CS 151: Lab 9

Title image Project 9
Fall 2019

Lab Exercise 9: Inheritance

The purpose of this lab is to give you practice in creating a base class and many child (derived) classes that inherit the methods and fields of a base class. In addition, we'll be modifying the L-system class to enable it to use rules that include multiple possible replacement strings and choose them stochastically (randomly).

Adding the capability to incorporate different replacements will enable you to draw yet more complex L-system structures where each tree is unique. We'll continue to use material from the ABOP book.


This week we'll modify the lsystem.py and turtle_interpreter.py files and create a new shapes.py file that implements shapes other than trees. Please follow the naming conventions for classes and methods. We'll provide some test code that expects particular classes, methods, and parameter orderings.

  1. Setup

    Create a new working folder. Copy your lsystem.py and turtle_interpreter.py files from your prior assignment (version 2). This week we are writing version 3 of both files. Make sure a comment at the top of the file indicates they are version 3.

  2. Context

    The change to the Lsystem class is to enable it to use rules with multiple possible replacements. During replacement, the particular replacement string is chosen randomly. There are a number of methods requiring modification: __init__, read, replace, and addRule. In addition, you'll need to import the random package.

    An example of an L-system with multiple possible replacements is given below. Note that the rule has the symbol F followed by three separate replacement strings, separated by spaces.

    base F
    rule F F[+F]F[-F]F F[+F]F F[-F]F

    When replacing an 'F' symbol, the replace function should select randomly from the possible replacement strings. This changes the form of our rules from 2-elements lists [symbol, replacement] to be N-element lists like [symbol, replacement_1, replacement_2, replacement_3].

    To make handling multiple rules easier, we're going to make use of Python dictionaries instead of a list to hold the rules. The dictionary key will be the symbol to be replaced, and the list of replacement strings will be the dictionary entry. Using a dictionary means the replace algorithm doesn't have to loop over the rules, it can simply use the symbol as the dictionary key to get its list of replacement strings.

    + (more detail)

    A dictionary is an implementation of a lookup table. Each entry in a dictionary has a key (like a word) and a value (like a definition). To create an empty dictionary we use curly brackets instead of square brackets like a list.

    mydict = {}

    To add a new entry to a dictionary, we index the dictionary with the key and make an assignment. The following creates an entry in the dictionary for the string 'F'. The value of the entry is a list containing the strings 'FF' and 'F+F-F'.

    mydict['F'] = [ 'FF', 'F+F-F' ]

  3. Update the Lsystem class

    There are three functions in the Lsystem class that need to be udpated to make use of multiple replacement strings and using a dictionary to store the rules: __init__, addRule, and replace.

    1. Update __init__

      In the __init__ method, change the initialization of the self.rules field to be an empty dictionary instead of an empty list.

    2. Update addRule

      Given a list of the form [symbol, replacement_1, replacement_2, ...], update addRule so that is puts an entry into the self.rules dictionary such that the symbol is the key and the list of replacement strings is the value.

      + (more detail)

      The addRule should no longer append a 2-element list to self.rules. Instead, make an assignment into the dictionary with a new key. The value of the new entry should be a list of all the possible replacments for a symbol.

      Consider the process from file to dictionary. For example, the line below is a possible rule for a tree.

      rule F F[+F] F[-F] F[+F][-F]

      After reading the line and breaking it into strings using split, the words variable in the read method would be the following.

      [ 'rule', 'F', 'F[+F]', 'F[-F]', 'F[+F][-F]' ]

      The read method will pass words[1:] into addRule as the newrule argument. Therefore, the newrule variable will be:

      [ 'F', 'F[+F]', 'F[-F]', 'F[+F][-F]' ]

      The symbol 'F' is the first item in the list (newrule[0]) and the set of replacements is the remainder of the list (newrule[1:]). Modify the addRule method so that it assigns the list of replacements, newrule[1:], to the self.rules dictionary with the symbol newrule[0] as the key.

    3. Update replace to use a dictionary and multiple replacement strings

      In the replace method, there is currently a loop over the list of rules, comparing the current character c to the rule's symbol rule[0]. If self.rules is a dictionary, the loop is no longer necessary. Likewise, the found variable, and the "if not found" statement after the for loop can also be deleted.

      Instead the logic will be if the self.rules dictionary has a key that matches the current character c, then use c to index into self.rules, randomly choose a string from the set of replacement strings, and concatenate it to the output string. Else, if the self-rules dictionary does not have a rule matching c, then concatenate c to the output string.

      + (more detail)

      The following is the structure of the new replace function. If you haven't used it before, the random module contains a function random.choice that selects randomly an element from a list.

      def replace( self, istring ):
        # assign to a local variable (e.g. tstring) the empty string
        # for each character c the original string (istring)
          # if the character c is in the self.rules dictionary
            # add to tstring a random choice from the dictionary entry self.rules[c]
          # else
            # add to tstring the character c
        # return tstring
    4. If your Lsystem read method uses mutators to update the information in the object, the read method should not change. Double-check that the code uses the argument words[1:] for self.addRule(), which passes everything but the first item in the words list.

    When you're done with these changes, try running the classtest.py test file from last week using one of the following L-system definitions. Do the trees all look the same?

  4. Update the TurtleInterpreter class

    Make three changes to the TurtleInterpreter class.

    1. Update __init__ so it will create only a single window

      For this project, we will be creating many TurtleInterpreter objects. Only the first one should create a new turtle window by calling turtle.setup(). To implement this, we'll use a class variable that starts with the value False and then changes to True after the first time a TurtleInterpreter object is created.

      Just after the class TurtleInterpreter: line, and inside the class, assign False to a variable called initialized. This variable is what is called a class global. Only one copy of it exists for all of the objects in the class. We're going to use it to keep track of whether the turtle window has been initialized.

      If the TurtleInterpreter.initialized variable is True, then a turtle window has been created, the __init__ method should immediately return. Otherwise, assign to TurtleInterpreter.initialized the value True and execute the remainder of the __init__ method.

      + (more detail)

      Add the following three lines to the beginning of the __init__ method. Note that we have to use the class name to access a class variable.

            if TurtleInterpreter.initialized:
            TurtleInterpreter.initialized = True

      Now the __init__ method will not create another turtle window if it already exists.

    2. Add characters to drawString to enable filling

      Add two cases to draw string. For the character '{' call turtle.begin_fill(), and for the character '}' call turtle.end_fill().

    3. Add a color method that sets the turtle color

      If you don't already have it, create a method color(self, c) that sets the turtle's color to the value in c.

    When you're done, download and run the following test function. You should get something like the following, but with randomly selected colors.

  5. Make a parent Shape class

    We're now going to start building a system that makes use of our turtle interpreter class to build shapes that are not necessarily the output of L-systems. Just as we made functions in the second project to draw shapes, now we're going to make classes that create shape objects.

    A shape object will hold all of the information necessary to draw a shape using the drawString function. Each shape will be defined by a string. In addition to the string, each shape will have a distance and an angle that tell drawString how to interpret the symbols. Each shape can also have a base color. All of this information will be stored in a Shape object.

    Create a new file shapes.py. In it, create a class called Shape. It is a base class, from which other classes will be derived. Then define an init method with the following definition.

      def __init__(self, distance = 100, angle = 90, color = (0, 0, 0), istring = '' ):
        # create a field of self called distance and assign it distance
        # create a field of self called angle and assign it angle
        # create a field of self called color and assign it color
        # create a field of self called string and assign it istring
  6. Create the following mutator methods

    • setColor(self, c) - set the color field to c
    • setDistance(self, d) - set the distance field to d
    • setAngle(self, a) - set the angle field to a
    • setString(self, s) - set the string field to s

  7. Make a draw method for the Shape class

    def draw(self, xpos, ypos, scale=1.0, orientation=0):

    The draw method will take in a position (x and y), scale, and orientation for the shape to be drawn. It should create a TurtleInterpreter [TI] object and then use the TI object to place the turtle at the given position and orientation, set the color, and draw the string. The string, distance, and angle attributes of the object should be used to call the TI drawString method. To implement scaling, multiply the distance argument by the scale parameter when passing it to drawString.

    + (more detail)

    Create a method draw that executes the following algorithm. Make sure the order of parameters matches. As you write your code, each argument to a TurtleInterpreter function should come from either the draw function parameters or the internal attributes of the Shape object.

      def draw(self, xpos, ypos, scale=1.0, orientation=0):
          # create a TurtleInterpreter object
          # have the TurtleInterpreter object place the turtle at (xpos, ypos, orientation)
          # have the TurtleInterpreter object set the turtle color to self.color
          # have the TurtleInterpreter object draw the string
          #    Note: use the distance, angle, and string fields of self
          #    Note: multiply the distance by the scale parameter of the method
  8. Create child classes to implement a Square and a Triangle

    In the same file, but below the Shape class definition, begin a new class Square that is derived from Shape.

    class Square(Shape):

    Have the __init__() method of the Square class take two optional arguments: distance and color. The __init__() method should do the following.

      def __init__(self, distance=100, color=(0, 0, 0) ):
        # call the parent's __init__ method with self, distance, 
        #      an angle of 90, color, and the string 'F-F-F-F-'
  9. In the same file, below the Square class, create a new class called Triangle that is the same as the Square class, but sets the string to 'F-F-F-' and sets the angle to 120.

  10. Test your code

    Once you have completed the Square and Triangle classes, download the second test function and run it. You should get the output below.

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