Project 2B - Falling Sand Simulation, Object Oriented

Download the part B starter files proj2B.zip

This folder contains the following files:

  • sand_oo.py - You will write your code for Part 2 here
  • Grid.py - This will originally be empty. You should copy your Grid class into this file
  • test_sand_oo.py - The autograder tests for this part of the project

sand_oo.py contains additional code that handles mouse clicks and drawing the GUI for the project. You will not have to modify this code, just the code that simulates the world.

Look through the code to familiarize yourself with the layout. Don't worry about any of the code currently there. You will not need to modify it but if you're interested in how the program works, you can explore the existing code. All the code you will write in this part of this project will be at the top of the sand_oo.py file where the comments indicate.

The next thing you will need is your Grid class. You should copy your Grid class into the empty Grid.py file, then add an import line at the top of sand_oo.py to import your Grid class. You'll be using it throughout this project.

Part 2

In Part 1, you solved the problem in a Functional Programming style. Your functions return new versions of the grid that you then used for the next steps. In this part of the project, we are going to rewrite the program to use a more Object Oriented style of coding so that you can begin to get a feeling for how they differ. You already have your Grid class. In this part of the project, you'll add a Sand class, and reorganize the code to use this new class. Instead of storing strings, we'll store Sand objects in the grid.

An object of the Sand class represents a single sand particle. Instead of having general functions that move sand, the Sand class will have methods that are responsible for moving itself and updating its position in the grid.

For this part of the project, you'll work in the sand_oo.py file. You've already written most of the code you'll need to make this version of the code work. It's just going to be organized a bit differently. Feel free to copy and paste code that you wrote in Part 1 or Homework 4 as needed. Let's get started.

Task 1 - Create a Sand class

You started this class in Homework 4. If you haven't completed Homework 4 yet, you should go do so.

At the top of the sand_oo.py file, add the code for the Sand class that you wrote in Homework 4. We often put class definitions in different files and then import them, but for this Project (and ease of grading), we'll have you put the class definition in the file where it will be used instead.

With the code you've already written added, you're ready to move on.

Task 2 - Adding and Removing Sand

Since the sand particles are actual objects instead of just strings stored in the grid, we need to do a little more to add and remove them from the simulation. In addition to storing reference to the Sand object in the grid, we will store references to all of them in a list as well. This will allow us to be more efficient in our updates of the grid but that's Task 3. For now we need to add code to add and remove the sand objects from the list and grid.

The sand_oo.py file has a list variable called all_sand. This is where we will store the list of all Sand objects. Additionally there are add_sand() and remove_sand() functions. Both of these functions take a reference to the Grid class as well as an x and y coordinate to either add or remove sand from. You won't ever explicitly call these methods, but you need to implement their functionality so that the GUI works properly.

In add_sand() you need to add functionality to:

  1. Verify that the position specified by the x & y coordinates is empty in the grid. If not, exit the function (do not exit the program).
  2. Create a new Sand object and set its position to the supplied x and y coordinates.
  3. Add the new object to the all_sand list.
  4. Store a reference to the new object in the correct grid position

In remove_sand() you need to add functionality to:

  1. Verify that there is a sand particle in the specified position. If not, exit.
  2. Remove the Sand object from the all_sand list.
  3. Remove the reference to the Sand object from the grid.

In some other languages, notably C++ you'd need to also delete the unused Sand object but Python will handle that for you as long as there are no longer any references to it in the Grid object or the all_sand list.

Task 3 - Moving the Sand

In the first part of this project, we iterated over all of the positions in the grid and if there was a sand particle, we moved it. In this version of the program, we don't have to do that. We can just iterate over the list of Sand objects. Then we know we will have visited each one once and only once and we also didn't have to access any of the grid positions that were empty or had rocks in them.

Again we are going to write the do_whole_grid() function. It should probably be better named move_all_sand() but in the interest of not rewriting the GUI interface code, we'll use the same function name.

In this version of the function, instead of looping over all the grid position, we just loop over all the sand particles. For each particle we call its move() method passing in the gravity() method as the physics to use. This will check each particle and move it if it can.

Iterating from bottom to top

Even though we don't need to iterate over the entire grid, we do need to visit the sand particles in a specific order. To see why, consider this setup:

Two rocks horizontally aligned with one space in between them

Then, with gravity turned off, imagine we add a sand particle:

A sand particle whose x is between the two rocks and whose y is one less than the two rocks

Then a second sand particle:

A second sand particle between the two rocks (directly below the first sand particle)

Finally, imagine we turn gravity on and catch the two sand particles while they're falling:

The two sand particles falling, now with a space between them

Wait what?! They have a space in between them! This happens because the upper sand particle appears in all_sand before the lower particle since it was added first, and so when gravity is evaluated for the first time, the upper particle has nowhere to go. The lower particle has not yet fallen and is in the way of the upper particle. But the lower particle itself can freely fall. Then, after the first frame (where the lower particle has fallen one square and the upper particle has stayed still), both particles fall unhindered until they hit the bottom. This is what creates the gap.

To fix this, we still have to visit the sand particles from bottom to top. That way, the lower particles can fall and get out of the way for the upper particles to fall as well (if possible). To do this, we can sort all_sand before iterating over it. Thus, the first line of do_whole_grid() is this:

all_sand.sort(key=lambda particle: (-particle.y, particle.x))

key is a function that applies a transformation to the objects in the list before sorting. First, each object's key is computed, then the list is sorted by comparing the keys to each other. In our case, the key is a tuple. The first element is the negative of the particle's y coordinate. It's negative so that when it's sorted in ascending order, the higher y values (which represent spaces that are lower in the grid) will be sorted before lower y values. The second element is the particle's x coordinate. This is just to standardize the order by making particles with lower x values appear before particles with the same y value and higher x values.

Task 4 - Interactive Testing

Once you have do_whole_grid() written and tested, along with all the other functions listed above in this part of the project, you can try running the whole program. You can do this with:

python sand_oo.py

Just like in part one, click the rock or sand buttons and then click the mouse button to scribble rocks or sand onto the world. If it is all working, great! If not, debug your code until it is.

Task 5 - Run the test cases

Now that you have everything working, you can try running the supplied test_sand_oo.py tests:

pytest test_sand_oo.py

This file contains a few tests that use all of your functions in sand_oo.py to process some predefined grids. If you've implemented everything correctly, the tests should pass. If they don't, there is something wrong and you'll need to go back and debug (and maybe write a few more test cases) to figure out the issue.

Submit Part 2

Congratuations, you've now created a second version of your program in a different programming paradigm.

You'll submit your sand_oo.py and Grid.py files on Canvas via Gradescope where it will be checked via the auto grader. We will be testing your code with some pre-defined initial grids to verify that you end up with the same final grid. We do not guarantee that all scenarios are tested by the test cases that we have provided you.

Going Further

  • In the do_move() function, how might you generalize it to move any type of particles and not just sand? Maybe you already did that in your implementation but if you didn't, what would you change?

© 2023 Brigham Young University, All Rights Reserved