Homework 4 - The Sand Class
Due by 11:59pm on May 24, 2025
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.physics()
>>> (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.
HintRemember 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 achieve 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 remember 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>)"
NoteDon’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 they 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()
returnsNone
, exit the function (you can do this by sayingreturn
) - 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:
block-beta columns 4 block:group1:2 columns 3 A["'r'"] B["'s'"] C["'r'"] space:3 D["'r'"] E[" "] F["'r'"] B --> E space a("(a) move down") space end block:group2:2 columns 3 G[" "] H["'s'"] I["'r'"] space:3 J[" "] K["'s'"] L["'r'"] H --> J space b("(b) move down-left") space end block:group3:2 columns 3 M["'r'"] N["'s'"] O[" "] space:3 P["'r'"] Q["'r'"] R[" "] N --> R space c("(c) move down-right") space end block:group4:2 columns 3 S["'r'"] T["'s'"] U["'r'"] space:3 V["'r'"] W["'s'"] X["'r'"] T --> W space d("(d) move blocked") space style W fill:#FF7276,stroke:#333; end space block:group5:2 columns 3 Y["'r'"] Z["'s'"] AA["'r'"] space:3 AB["X"] AC["'s'"] AD["'r'"] Z --> AB space e("(e) move blocked
(corner rule)") space style AB fill:#FF7276,stroke:#333; end space
Check to make sure the sand particle isn’t trying to move out of the grid. That would be problematic for obvious reasons.
We should also check to see if the place the particle is trying to move to is
occupied. If it is occupied, is_move_ok()
should return False
. For example,
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 False
if the space below
the sand particle is occupied, because it can’t move there.
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).
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
.
In this physics()
method, 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. The autograder is
looking for working __str__
, physics
, and move
methods.