Waterdrop game, the second version

In the first version of the waterdrop game, the idea was that waterdrops on the gameboard splash water to other drops when they popped, and our goal was to clear the water from the board. We will now continue working on the waterdrop game and implement a new version of it using the new things we have learned, such as pointers.

In the first version, communication between waterdrops meant that when a drop popped, the square object directly added water into the square objects which would be splashed by the water from the pop. It did not, however, resemble the functioning of the original version of the game, because in that one, the splashes moved on the board one square at a time. That way, a splash reaches a square next to it sooner than it does a square on the other side of the board which, again, affects the popping order of the drops, for example.

Modules and data structures of the game

Our new implementation will have waterdrop objects on the board, instead of square objects. The difference to the first version is that the first one had an object in each square of the board. This time, we implement a version with each square having a pointer in it. If a square has a waterdrop, the square contains a pointer pointing to the drop object. If the square is empty, the pointer in the square is nullptr.

The figure below shows the board from the 4th round (on the left), and the one we are going to implement now (on the right).

On the left, the board from the 4th round, and on the right, the one we are going to implement.

Besides drop objects, the new version has splash objects that move on the board. A waterdrop object that pops will create four splash objects moving on the board and carrying the added water to a point of the board where they meet another waterdrop object. So, the new version has more objects that communicate with each other. Besides modularity, we will get an example on object-oriented programming.

It would be difficult to implement all the functionality by using only waterdrop objects and splash objects. We will also need a module that is responsible for the functioning of the game as a whole. We will call this module the gameboard. As a result, we will have the following division into modules:

  • The class Drop implements a single waterdrop, which means it knows its location and the amount of water in it, and is responsible for popping the drop at the right time.
  • The class Splash implements a single splash. It knows its location and which way to proceed.
  • Unlike the previous objects, the class GameBoard has only one instance, managing the drop objects and the splash objects.
  • The main program module creates the gameboard object and handles the user input.

Before getting into the code in the directory examples/10/waterdrop_game_v2, you should execute the program at least once, and thus, copy it to your student directory. The functioning of the program code is easier to understand after you have seen how it is presented on the screen in ascii graphics.

Communication between objects

The program is complicated and there is no absolute need to study it all the way through. In order to understand the viewpoint of object-oriented programming, we will go through what happens when we drip one waterdrop on the board.

Enter the seed value 7 to the random number generator, and then enter 4 11 as the input coordinates. Examine the output of the program and the program code to find out what goes on while the program is executed. In the program code, you should find the following points:

  1. The function main calls the gameboard method addWater that notices that the square (4, 11) has a pointer pointing to a drop object. The gameboard object calls the addWater method of the drop object in question.

  2. The drop located at the coordinates (4, 11) pops. It creates four splash objects, all located in the square (4, 11), but each of them having a different direction. The method addWater receives the vector splashes_ of the gameboard as a reference parameter, which means that this vector is the target of push_back calls, made in the method. In other words, the new splash objects go directly onto the gameboard.

  3. The function main calls the print method of the gameboard, and the print method first calls the method moveSplashes.

  4. The method moveSplashes goes through all the splash objects in the vector splashes_ of the gameboard, and calls the method move for each of them.

  5. The splashes going up and down move into the squares containing a waterdrop. The method moveSplashes notices the action and calls the method addWater for the correct waterdrop object and destructs the splash object. You can see this phase in the following board print such that the waterdrops above and below of the square (4, 11) have grown by one unit.

  6. As a result of calling move for the other two splashes, the directions of which are left and right, the splashes end up in squares containing nullptr, which means that there are no drops in these squares. In printing the board, this is depicted with the characters < and > at the locations of these splashes.

    Printing the board is more complicated than in the round 4 program, because now we cannot print an empty square with a space, but we first must find out if there is a splash or even several splashes in that square. The choosing of the print character of an empty square is implemented in a function of its own in order to clarify the print function. Most likely you will understand how the program works, even without examining the method droplessSquareChar.

  7. The print function of the gameboard repeats the print in a do - while loop as long as there is at least one splash in the vector splashes_.

    One splash on the board advances the square (2, 11) such that the print method of the board calls moveSplashes before each print. The same holds true for the other splash moving towards (6, 11), but let us for a while pay attention only to the firstmentioned splash moving left.

  8. When the splash is in the square (2, 11), the method moveSplashes notices it and adds water into the drop object in that square using its method addWater and destructs the splash object.

  9. Adding water to the drop object in the square (2, 11) also makes it pop, and the drop object creates four new splash objects. The method moveSplashes, however, did not pass the vector splashes_ as a parameter to the method addWater, instead it passed another temporary container. This is why these splashes do not start moving in the loop structure of the moveSplashes method right away. As its last action, moveSplashes stores the new splashes into the vector splashes_ of the gameboard, which is why the print of the board goes on.

  10. The next time we print the board, there are four splashes in the square (2, 11). The print function calls the method droplessSquareChar that returns the ”mess character” * in the square (2, 11), because you cannot print four splash characters going in four different directions into one square. Therefore, this ”mess character” depicts, in a way, the popping of a waterdrop.

  11. The printing of the gameboard goes on in the way that first, the print function calls the method moveSplashes. As the splashes move the way we explained above, three of them hit the drops, and the one going to the right hits an empty square.

  12. Very similar things happen at (6, 11). However, here only two splashes hit the drops, namely the splashes going up and right, and the splashes going down and left hit an empty square.

  13. When the right-moving (from (2, 11)) and left-moving (from (6, 11)) splashes enter the square (4, 11) at the same time, you again see the ”mess character” * that will, this time, not depict a new pop but the fact that drops headed to different directions were in the same square for a moment.

All the splashes continue moving on the board in this way for a few board prints, until all the splashes have disappeared.

Responsibilities of drop objects and splash objects

When you examined the example above, you noticed that the structure of the program is quite complicated. It makes us wonder if we really did gain something from the multitude of objects. Would it not have been better if the gameboard had simply included the data on how much water the drops had and where the splashes went and the gameboard object had controlled everything?

Colliding splashes

An example of how it can be useful to have objects that are responsible for small things is the previous execution, where two splashes proceed to opposite directions and meet each other in one square. Looking at the class Splash, you might wonder how the meeting of two (or more) splashes could complicate the movement algorithm of a splash, but now you see it does not. The class Splash controls the movement of a single splash object. Even if two objects met in the same square, it would not make the functioning of one of them any more complicated. If we, instead, had a gameboard object controlling all the details about drops and splashes, this could affect the splash movement algorithm in a different way. This teaches us that the idea of object-oriented programming is to create small parts that are independently responsible for their own functioning.

Destructing objects

Earlier, we already talked about the gameboard object destructing the splash objects in the method moveSplashes. Destructing drop objects, however, is very different. If the drop method addWater exceeds the maximum size and it pops, the drop object will destruct itself after creating the splashes. It is done by the drop object calling the the gameboard method named removeDrop.

It is important that the self-destruction is the last thing happening in the method, because the functioning of an object is undefined after its memory has been deallocated.

This difference in destructing splash objects and self-destructing drop objects is a good depiction of what the responsibility of a module means. The drop object self-destructs independently. This makes it easier to implement the gameboard object. Instead, the method moveSplashes of the gameboard object does many actions we could just as well dedicate to the splash object. If the implementation was different, we could have the splash object add the water to the drop object it hits, and then destruct itself. Since the gameboard object performs some of the actions of the splashes, it has grown larger than it perhaps should. On the other hand, if we gave more responsibility to the splash object, it would need access to the gameboard, and that would make the data structures of the program even more complicated. There are several viewpoints to each of the different solutions. A single right solution does not exist. As a programmer, you must be able to weigh the pros and cons of different solutions.

Summary

While being far from perfect, the implementation of this program offers you a great opportunity for philosophical thought about the good and the bad of the module division in this implementation. Did we make wise decisions when choosing which functionality is the responsibility of which module? Does each module respond the things it is responsible for? If we changed the division of responsibilities, would it make the program more or less complicated?

These issues will be discussed further on the programming courses following this one (e.g. Programming 3: Techniques and Software Design). However, if you are interested in this topic, it is worth thinking about it when you create other projects of your own.