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 the Particle object (store self)

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:

sand movement examples

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 Bubbles and Rocks) 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.

© 2024 Brigham Young University, All Rights Reserved