Program 5

Inheritance and Simulation

ICS-33: Intermediate Programming


Introduction This programming assignment is designed to give you experience reading and using an inheritance hierarchy, by extending its base classes. In this assignment you will also see how to write general GUI applications by using the Model-View-Controller pattern; although you will see GUI components using tkinter, you will not write any of the GUI components, just the Model part of the pattern. The application allows the user to place different kinds of objects (called simultons), which behave/display differently, into a simulation and watch them interact.

You should download the program5 project folder and use it to create an Eclipse project. This download contains many modules that are already written, and others that you must write: a process simplified by using inheritance. The main goal here is to use inheritance to write small classes that usefully inherit the maximal amount of behavior and state, requiring you to write only the minimal amount of code/instance variables for them to behave and display as required.

You should also download the program5helper which is a working simulation that is a more primitive version of the code that you must write. It contains lots of good ideas (in code) about writing simulations, but uses no inheritance. Writing your simulation (especially the model module) will be simpler if you study and understand this example.

To run either the regular or helper simulation, you must run the script named script.py which creates the view and controller by importing their modules (and controller imports model).

This is our final programming assignment. You should work on this assignment by yourself, not in pairs. You can discuss aspects of it verbally with other students in the course, draw diagrams, etc. but you should write all the code yourself; of course, you can post questions on the forum (and read questions/answers that other students post). For this assignment, every student in class will submit all parts of his/her assignment; because there are so many files, I'm asking you to zip your entire project folder, so we can unzip and run it; don't leave anything out (and don't refer to any modules that are not included in the zip file).

The comment at the top should look something like

# Pat O'Neil Riley, Lab 1
# I certify that I written all the code in this programming assignment
#   by myself.

Grading will be based on a visual inspection of your program, and watching the behavior of the simulation when it runs.

Print this document and carefully read it, marking any parts that contain important detailed information that you find (for review before you turn in the files). The code you write should be as compact and elegant as possible, using appropriate Python idioms.


Problem #1: Inheritance and Simulation

Problem Summary:

I have written the view and controller modules for this simulation; I have also included some base classes used in the model module. You will write the complete model module, and a variety of classes derived from the base classes by inheritance. You will also use the names controller.the_canvas and controller.the_progress. The former starts with a width 500 pixels and height 300 pixels (the origin, (0,0) is in the upper left-hand corner, with x values getting back going right and y values getting bigger going downward); the user can resize the window and your simulation should respond accordingly. The later is a label whose text can be updated by calling the config method.

The inheritance hierary looks as follows (arrows go from sub-/derived-classes up to their super-/base-classes. All classes that do not specify base classes have object as their base class. Most classes have just one base class: only the Hunter class inherits from more than one base class.

Here is a brief description of each class, discussed in more detail later. Recall that you will also be writing the model module, which does not appear in this hierarchy but uses the classes in the heirarchy.

  • The Simulton base class stores the location (x,y coordinates of the center) and dimensions (width and height) of every object in the simulation. It includes methods to query and update this information.

  • The Mobile_Simulton base class stores the angle (in radians) and speed (in pixels/update) of every mobile object in the simulation. It includes methods to query and update this information.

  • The Prey base class is an ancestor of every class that produces "edible" objects; it contains only code to call __init__ for Mobile_Simultons (it base class).

  • The Black_Hole class represents a stationary object that eats (removes from the simulation) any Prey object whose center is contained its perimeter

  • The Pulsator class represents a special kind of Black_Hole: one that gets bigger (as it eats Prey) and smaller (as it starves); if it gets too small (starves for too long) it dies: removes itself from the simulation.

  • The Ball and Floater classes represent balls (traveling in straight lines) and floaters (travelling more eratically) that traverse the simulation canvas. They are both subclasses of Prey so they can be "eaten" by Black_Holes, Pulsators, Hunters,

  • The Hunter class represents a special kind of Pulsator: one that is mobile (hence two base classes), and moves towards the closest Prey that it can see.

When you define these classes, you should not modify any code written in their base classes. You should also not access by name or duplicate an instance variables (attributes) that are inherited. You can call methods in the bases classes to access/update their instance variables and override any methods in the base clases (whose bodies might call the overridden methods to help them accomplish their task). You can also define new methods. Each leaf class must define or inherit an update and display method, which respectively implement the behavior of objects in that class and the image they display on the canvas.

Finally, you must develop one more class, named Special that appears somewhere in this hierarchy, and exhibits interesting behavior. Feel free to define supporting module/classes if necessary.

I suggest that you start by looking at the code in the modules that appear in the project folder that you will download. Then (see the detailed instructions in this document) you can add/test/debug each of the derived classes in the hierarchy, starting near the top and moving downward to the bottom, until all classes are implemented and behaving as they should.

Details

I have written the Simulton, Mobile_Simlton and Prey classes. Note that the Simulton class defines a contains method that is inherited and can be overridden (and should be, in Black_Hole). This method determines whether a (x,y) coordinate is inside the object by checking whether it is contained in the bounding box of each object: the bounding box is just all coordinates extending from the object's center, going half-height up and down, and half-width left and right. The Black_Hole class overrides contains to be whether an (x,y) coordinate is inside the perimeter of the object.
  • You should start by writing the Ball class, which should be simple because of the state/methods it inherits: each Ball is blue, has radius 5, moves at 5 pixels/second, and starts moving at a random angle.

  • Next write enough of the model module to test this module and the Ball class. Write the world, reset, start, stop, step, select_object, mouse_click, add, remove, find, update_all, and display_all functions, most of which can be tested using only Ball objects; as you write more classes later, you will continue to call/test these functions. The step button stops the simulation after executing one cycle: if it is running, it stops after one more cycle: if it is stopped it starts for one cycle and then stops again. The select_object function remembers (using a global name) the string of the button clicked (which it is passed as an argument: see object_button in the controller module; the mouse_click function creates an object from the last remembered selection at the (x,y) coordinates of the click: using eval makes this method small, and easily extendable to other classes of simultons. The __init__ method of every Simulton subclass is called with only the (x,y) 2-tuple, supplied by the mouse_click function.

    Recall that a method or function can use the value of any (global) module variable in its body, but if we need to update the binding for any (global) module variable, we need to declare that (global) module variable global in the method/function. You can help debug your model module functions by having them print useful information in the console when they are called.

  • Write, test, and debug the Floater class, which is similar to the Ball class, with two important differences: they are displayed as images (a UFO icon) and they move in a strange way. Insure the user can add Floaters to the simulation. Initially, each Floater moves at 5 pixels/second, and is moving at a random angle.

    IMPORTANT: To process the GIF file for a flying saucer, you need to download and install Pillow (PC 32 bit) (a modified version of PIL: a fork -available for PCs and Macs, but I haven't found a good place to download the Mac version). I have used PIL but not Pillow: please feel free to post on the forum any useful information about downloading, installing, and using this software. It is even OK to post code (like what I show below) for using these modules. Generally, UCI maintains a great resource for Python PC modules, which includes links to these and other versions (64 bit) of these modules.

    IMPORTANT: You are NOT required to download/install either of these, and you are welcome to implement floaters similarly to balls: using a radius 5, but as red circles; you might want to implement floaters this way first, and then try to switch to the flying saucers only if you have time

    To process the Floater's image

    1. To access the PhotoImage class from PIL.ImageTk we must import PhotoImage and store the image returned from PhotoImage(file='ufo.gif') whose .gif file must be in the project folder. We can call the width() and height() methods to compute the dimensions of the image.
    2. Use the create_image function to place the image at its location on the_canvas that is defined/set in the Controller module (similar to the create_oval method used in the Ball class).

    To move the Floater

    1. Use random numbers so that 30% of the time the speed/angle are changed.
    2. The speed is changed by a random value betwen -.5 and +.5, but never drops below 3 pixels/update or rises above 7 pixels/update; and the angle is changed by a random value betwen -.5 and +.5 radians.

  • Write, test, and debug the Black_Hole class. Ensure the user can add Black_Holes to the simulation. Each Black_Hole is black and has a radius of 10. Override the contains method so that a point is contained in the Black_Hole if the distance from the center of the Black_Hole to the center of the object is less than th radius of the Black_Hole. Use the find method in the model module to locate all objects that are instances of Prey (or any of its subclasses no matter how many are added later) and whose locations are contained in the circle representing the Black_Hole. The update method should return the set of simultons eaten (this will be useful for the Pulsator class, which extends the Black_Hole class.

  • Write, test, and debug the Pulsator class. Ensure the user can add Pulsators to the simulation. Each Pulsator behaves and initially looks like a Black_Hole, except for the following additional behavior. For every object a Pulsator eats, its dimension (both width and length) grows by 1 and its "time between meals" counter is reset; whenever it is goes 30 updates without eating anything, its dimension (both width and length) shrinks by 1; and if the dimesions ever shrink to 0, the object starves and removes itself from the simulation. The update method should still return the set of simultons eaten.

  • Write, test, and debug the Hunter class. Ensure the user can add Hunters to the simulation. Each Hunter behaves and initially looks like a Pulsator, except for the following additional behavior. Use the find method in the model module to locate all objects that are instance of Prey (or any of its subclasses no matter how many are added later) and whose locations are within a distance of 200 of the Hunter (hint: see the methods in the Simulton class); if any are seen, find the closest one and set the hunter's angle to point at that simulton: to hunt it. Hint: To determine the angle, compute the difference between the y coordinates and the difference between the x coordinates of the center of the closest prey simulton minus the center of the Hunter. Instead of dividing them to compute the tangent of the angle between them (and then calling math.atan to compute the angle), just call the math.atan2 function (with these differences as separate arguments) to determine the angle the Hunter should move to head towards the prey. By using math.atan2 and avoiding the division, there will not be a "divide by 0" problem, if the prey is directly over the hunter (have the same x coordinate):

  • Write, test, and debug the Special class. Ensure the user can add Specials to the simulation. Make the Special objects do something interesting; write a comment at the top of the special.py module that describes their behavior so the TA can read it and watch that behvaior when running a simulation. If you need to, you can modify you model module or other classes to interact with these Special objects. The TAs will award one extra credit point to the most interesting Special submitted in each lab.

    Testing

    If the program is developed in the manner described above, testing for the model module is done after its code and code for the Ball class are written. The step function is useful for "slowing-down" simulations to better observe the behavior of the simultons. As each new class is written and entered into the simulation, its behavior and appearance are tested (possibly correcting functions in the model module as they are used more extensively in later classes).

    You might find it useful (but it is not required) to write __repr__ and/or __str__ classes for each class and print them occassionally for debugging purposes.

    You can download and watch (~4 minutes) a narrated demo of me running my solution, to see how the simultons behave. Unfortunately, it does not show the mouse cursor, but when buttons are clicked they momentarily appear depressed. Here is the script for the movie.

    1. The simulator begins stopped, with no objects on the canvas.

    2. I can click the Ball button, and then click anywhere on the canvas, multiple times, to place balls.

    3. I can click the Start button to start the simulation. Notice how the balls move in a straight line and bounce off walls. I can click on the canvas to add more balls.

    4. I can click the Stop button to stop the simulation.

    5. I can click the Start button again to restart it from where it was stopped.

    6. I can click the Step button to run the simulation for one cycle. I can click this button any number of times; each click advances one cycle. This button can be useful for debugging.

    7. I can click the Remove button. Then the next time I click on the canvas, any object that I click inside will be removed from the simulation; this feature works whether the simulation is running or not, but is easiest to use if the simulation is stopped.

    8. Finally, I can click the Reset button to reset the simulation. It is stopped with all simultons removed.

    9. I have now demonstrated every kind of button in the simulation. Let's look more closely at how other kinds of simultons behave and display.

    10. I can restart the simulation and click the Floater button to place floaters on the canvas. Notice how each moves differently than a ball, while still bouncing off walls.

    11. I can click the Black_Hole button and place a black hole on the canvas. Notice how it eats any simulton whose center enters its perimeter.

    12. I will now remove the black hole.

    13. I can click the Pulsator button and place a pulsator on the canvas. Notice how when it eats a simulton it grows; if it doesn't eat any simultons in 30 cycles, it shrinks and ultimately it can remove itself from the simulation if its size shrinks to 0.

    14. I can click the Hunter button and place a hunter on the canvas. Notice how it pursues prey (within its range of vision) and grows and shrinks like a pulsator.

    15. Finally, I can reduce or enlarge the size of the window and simultons adapt their behavior to the smaller or larger canvas.