Project 9: Pinball
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.
- Create a Block class
The first task is to create a Block child class using inheritance. The parent class is Thing).
- Define the Block __init__ method
The __init__ method should have the following signature. You are welcome to add additional optional arguments.
def __init__(self, win, x0=0, y0=0, width=2, height=1, color=None):
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 the dx parameter to the field self.dx and the dy parameter to the field dy. You can use self.width and self.height if you wish (or any other names of your choice).
Finally, call self.reshape() and self.color() to set up the vis list and set the color.
- Create the reshape method
The reshape method should undraw the graphics objects if they are drawn. Then it should define the self.vis list of graphics objects using the current fields. Then it should draw the graphics objects if they are drawn. 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).
- Write getWidth and getHeight
These two functions should return the width or height of the block.
- Write setWidth and setHeight
These two functions should update the value of the width (dx) field or the height (dy) field and then call the reshape method.
- Define the Block __init__ method
- Make another shape class of your choice
Choose a shape. It can be a polygon (e.g. a triangle or a pentagon) or it could be a multi-object shape like a snowman. The new shape class should inherit thing. The __init__ method should make sure all the necessary information is provided and finish by calling reshape and setColor.
The reshape 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 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.
If the class has a complicated color scheme, you may need to write a setColor for the child class.
If the class is a multi-shape object, you may need to write a setPosition function for the child class.
- Write a unified collision function
The next task is to modify the collision.py file to make it simpler to call the right collision function no matter what types of objects are involved. (Note, the collision functions all work with a ball and something else. We don't yet have block-block collisions, for example.)
The idea is to use a dictionary with the two types 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 entry that contains all the possible keys 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 with the key ('ball', 'ball') 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 call the collision_ball_ball function using the following syntax.
collision_router[ ('ball', 'ball') ](thing1, thing2)
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 of a ball and another type: ball, block, and the additional shape you created (which should be treated as either a ball or a block.
Finally, create a function called collision(ball, thing) that takes in two Thing objects plus a time step and uses the collision_router dictionary to call the right function. Assume that the ball is always the first parameter and the other object (ball, block, or other) is the second parameter.
- 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 ball into the scene and have it bounce around. But you can 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 # assign to frame the value 0 # create a ball, give it an initial velocity and acceleration, and draw it # start an infinite loop # if frame modulo 10 is equal to 0 # call win.update() # using checKey, if the user typed a 'q' then break # if the ball is out of bounds, re-launch 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 # increment frame # close the window if __name__ == "__main__": main()
Doing visual updates only every 10th frame will make the animation smoother. What that does is run the virtual simulation for 10 time steps and then redraw the objects in their new positions. You can adjust both the time step and how often the visualization is redrawn 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.
- Create one more shape class
Create a new class that makes a new shape with at least two graphics objects. Consider this object to be of type ball for the purpose of collisions. Replace the ball in your obstacle scene with the new shape (so the new shape should be bouncing around).
Include a link to a short video of your new shape bouncing around in your report.
- What is inheritance?
- What does it mean for a child class to override a method?
- What is a class variable or class global variable?
- What is a field of an object?
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. Be sure to describe any extensions you complete in your report.
- Create more elaborate spaces and develop interesting additional simulation videos.
- Create more shape classes and show that they collide and simulate appropriately.
- This simulation does not take into account masses. A challenging extension would be to modify the collision code so that when two things collide the velocity changes take into account the masses. (Conservation of momentum.)
- Add the capability to whack the ball to your pinball simulation.
- Create a separate scene like putt-putt golf and let the user shoot the golf ball.
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 project6, 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.
- Is your name at the top of each code file?
- Does every function have a comment or docstring specifying what it does?
- Is your handin project directory inside your Private folder on Courses?
Write your project report
For CS 152 please use Google Docs to write your report. Create a new doc for each project. Start the doc with a title and your name. Attach the doc to your project on Google classroom. Make sure you click submit when you are done. The graders cannot provide feedback unless you click submit.
Your intended audience for your report is your peers not in the class. From week to week you can assume your audience has read your prior reports. Your goal should be to be able to use it to explain to friends what you accomplished in this project and to give them a sense of how you did it.
Your project report should contain the following elements.
A brief summary of the project, in your own words. This should be no more than a few sentences. Give the reader context and identify the key purpose of the assignment.
Writing an effective abstract is an important skill. Consider the following questions while writing it.
- Does it describe the CS concepts of the project (e.g. writing well-organized and efficient code)?
- Does it describe the specific project application?
- Does it describe your the solution or how it was developed (e.g. what code did you write)?
- Does it describe the results or outputs (e.g. did your code work as expected)?
- Is it concise?
- Are all of the terms well-defined?
- Does it read logically and in the proper order?
- A description of your solution to the tasks, including any text output or images you created (including the required images/videos mentioned above). This should be a description of the form and functionality of your final code. Note any unique computational solutions you developed or any insights you gained from your code's output.
- A description of any extensions you undertook, including text output or images demonstrating those extensions. If you added any modules, functions, or other design components, note their structure and the algorithms you used.
- The answers to any follow-up questions (there will be 3-4 for each project).
- A brief description (1-3 sentences) of what you learned. Think about the answer to this question in terms of the stated purpose of the project. What are some specific things you had to learn or discover in order to complete the project?
- A list of people you worked with, including TAs and professors. Include in that list anyone whose code you may have seen, such as those of friends who have taken the course in a previous semester.