Homework 4 - The Sand Class
Due by 11:59pm on 2023-10-18.
Example Input and Output
grid = Grid(6, 6)
part = Particle(grid, 1, 2) #each particle has a grid reference
str(part)
>>> 'Particle(1,2)'
sand_particle = Sand(grid, 0, 0)
str(sand_particle)
>>> 'Sand(0,0)'
sand_particle.phsyics()
>>> (0,1)
Starter Files
Download homework04.zip. Inside the archive, you will find starter files for the questions in this homework.
Introduction
In Homework 3, you created a Grid class that we can use to store various types of data (such as rocks and bubbles) in a 2 dimensional way. In this homework, we are going to get started on the other object we need for Project 2, the Sand
object. These Sand
objects are going to be one of the things we store in our Grid
objects for our simulation in Project 2.
However, we are eventually going to make Rock
and Bubble
objects too, so instead of jumping straight into the Sand
object, we are first going to start by creating a Particle
class which we can use as a template for creating our Sand
class in this assignment. An object of the Particle
class will represent a single particle in our simulation. We'll set it up with all the information it needs to keep track of its position and parent Grid
so that once we want to create the Sand
class, we only need to implement a function for gravity.
Required Questions
Task 1 - Create a Particle class
Start by opening the empty Particle.py
and in that file create a new Particle class. The __init__()
method should take three parameters in addition to self
: a reference to the Grid
, and an initial x and y position. These latter two should have default values of zero so it should look like this:
def __init__(self, grid, x=0, y=0):
In the __init__()
method, store the reference to the grid in a class attribute and also create class attributes for the x and y positions and store the passed in (or default) values.
Note: Remember that when we are talking about classes and objects, functions are called "methods", and variables are called "attributes"
Add a __str__()
method that returns a string with the class name ("Particle" in this case) and the position of the particle like this, where <x>
and <y>
are the coordinates of the given particle:
Particle(<x>,<y>)
However, we don't want to have to rewrite the __str__()
method for every different kind of particle that we create. When we create a Sand
class, which will inherit from the Particle
class, we want its __str__()
method to automatically switch from printing Particle(<x>,<y>)
to Sand(<x>,<y>)
. We can acheive this by using another dunder attribute called __name__
.
Every class has a __name__
attribute which returns its name. So with our particle class, Particle.__name__
would return "Particle". However, we must also rememeber that when we create a Particle object, that object is an instance of the Particle
class, and not the Particle
class itself. Therefore, to reference the Particle
class from a Particle object, we can use type(self)
, which will return the Particle
class. Putting all the pieces together, we now want our __str__()
dunder method to look something like this:
def __str__(self):
return f"{type(self).__name__}(<x>, <y>)"
Note: Don't forget to replace
<x>
and<y>
with the x and y coordinate of the Particle, then give the__str__()
method a try to see what it prints.
Task 2 - Moving a Particle
Next we are going to create a move()
function. This function will take care of updating a particle's position when we tell it to move. How will we tell the particle where to move? Well, since in the long term we want to create Sand
, Bubble
, and Rock
classes which all are based on the Particle
class, we will write methods when we create those classes which will tell the particle move()
function where to move the particle. The Sand
, Bubble
, and Rock
classes will all have their own separate physics()
methods, but will all share the same move()
function which comes from the Particle
class.
The future Sand
, Bubble
, and Rock
classes will all have a physics()
method which will return either the position for the particle to move to (like this: (<x>, <y>)
) or None
if the particle cannot move.
With that in mind, this move()
function should do the following:
- Call
self.physics()
to get the position to move to - If
self.physics()
returns None, exit the function - Set the value in the grid at the particle's current position to
None
- Update the
Particle
object's position (class attributes) to the new position - Set the new position in the
Grid
to a reference to theParticle
object (storeself
)
Task 3 - Making the Sand Class
Now we are going to actually create one of the other classes that we have been talking about. Open the empty Sand.py
file that you have been provided, and begin by importing your Particle
class from your Particle.py
file. Next create the Sand
class, and make it inherit from the Particle
class like this:
class Sand(Particle):
By declaring the class like that, we essentially tell python to give our new Sand
class access to all the methods and attributes (functions and variables) that the Particle
class has. This means that without us writing anything in our Sand
class, it already has access to the __init__()
, __str__()
, and move()
methods from the Particle
class. All that we have to do now, is write the methods that tell sand particles how to move, and we will have a working Sand
class.
First, let's write an is_move_ok()
method which will check if the sand particle is allowed to move to a given position. It will take two parameters, x
and y
, which are the x and y coordinates where the sand particle wants to move. The function will then return True
or False
depending on whether or not the sand particle is allowed to move there.
Here are the rules for determining where a sand particle can move:
So if the sand particle is trying to move straight down (the passed in x is the same as the sand particle's x and the passed in y is 1 greater than the sand particle's y), then is_move_ok()
should return true if the space below the sand particle is empty, and false otherwise. If the particle is trying to move diagonally left or right, is_move_ok()
must check both the space that the particle is trying to move to AND the space which is above the destination, to the side of the space where the sand is moving from (see bottom example).
Also remember to make sure that you are checking to make sure the sand particle isn't trying to move out of the grid. That would be problematic for obvious reasons.
Lastly, let's add a physics()
method. This method will return the position the sand particle should move to when it is time for the particle to move. This method doesn't take any parameters and returns a tuple containing the x
and y
coordinates that the particle should move to. If there is no valid position to move to, the method should return None
.
The sand particle should first try to move straight down, then diagonal left, then diagonal right. Use the is_move_ok()
method that you just wrote to check if a move is okay. If a move isn't okay, try the next possible move following the order mentioned above. Once you have found a move that is okay, return a tuple containing the x and y coordinates that the particle should move to. If none of the possible moves are okay, simply return None.
Conclusion
We have now created a Particle
class which contains some methods which can be universally used by all particles (such as Sand
). We then created a Sand
class which makes use of the methods we wrote in the Particle
class along with a few methods we added to the Sand
class in order to support all the functionality we need from a sand particle. In the upcoming project, you will write the code for some more particle types (such as Bubble
s and Rock
s) and then write to code to make the particle simulation work.
Submit
You'll submit Grid.py
, Particle.py
and Sand.py
on Canvas via Gradescope to be graded.