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
.
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/01-16/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:
The function
main
calls the gameboard methodaddWater
that notices that the square (4, 11) has a pointer pointing to a drop object. The gameboard object calls theaddWater
method of the drop object in question.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 vectorsplashes_
of the gameboard as a reference parameter, which means that this vector is the target ofpush_back
calls, made in the method. In other words, the new splash objects go directly onto the gameboard.The function
main
calls the print method of the gameboard, and the print method first calls the methodmoveSplashes
.The method
moveSplashes
goes through all the splash objects in the vectorsplashes_
of the gameboard, and calls the methodmove
for each of them.The splashes going up and down move into the squares containing a waterdrop. The method
moveSplashes
notices the action and calls the methodaddWater
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.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 containingnullptr
, 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
.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 vectorsplashes_
.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.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 methodaddWater
and destructs the splash object.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 vectorsplashes_
as a parameter to the methodaddWater
, instead it passed another temporary container. This is why these splashes do not start moving in the loop structure of themoveSplashes
method right away. As its last action,moveSplashes
stores the new splashes into the vectorsplashes_
of the gameboard, which is why the print of the board goes on.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.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.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.
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.