Post #15 TETRIS in ABAQUS – Part 4
The final part on this mini-project – controlling the game.
In Part 2 we saw how the GUI called kernel functions in response to user input in the dialog and ran everything through the GUI’s processUpdates function to keep all the events synchronised. We shall now explore how the shapes are moved, how their positions are controlled to prevent them going too far, how contact between shapes is established and how the completed rows of blocks are removed to achieve a score.
Moving Blocks
The basic action in the game is translation of a group of cubes that are already arranged in a shape representing a block. This translation can be automatic, if a certain period of time has passed, or triggered by the user pressing a button. User input can move the block up or down, or alternatively it can force the block to travel down the playing area until it reaches a contact.
In all cases the translation function is the same:
The translation is defined by the string argument supplied selecting from a dictionary of translation vectors, translations, see yellow highlights, Figure 1.
The translation is applied to a list of cube instances that represents the current block, purple highlights, Figure 1. This list, instanceNames, is held in the model’s customData space and is created by the createShape function, see Part 3 of this series.
The assembly level translate function always requires a list of instance names, even if only one instance is being moved, and a vector. All the instances will move together. This is very convenient for our purposes here since the arrangement of the cubes that makes the playing block is preserved and no checks are required.
Checks are required to ensure that the movement that is about to happen is allowed, or whether it is blocked by the presence of an existing block. This check is done in CheckLimits, line 28, green highlight, Figure 1, before the translation is applied to prevent the block translating in the +/- Y direction through a block that they are already touching.
The method is simple. A list of face centroids is maintained for all the existing cubes in the playing space, mdb.customData.limits. Initially this list is empty. Once the first block, or collection of cubes, has been placed, the list is updated with the centroid of any cube face that has a normal pointing in the +/- Y direction. CheckLimits, Figure 3, compares the face centroids of the current shape’s cubes with this list. If a match is found it then gets the face unit normal. If the face has a unit normal pointing parallel to the Y-axis, then the inverse of the Y component, which represents the desired direction of travel that the face would block, is added to a list that is returned. The Y component of the desired translation can then be checked against this list and if it is not found the translation can be performed, see purple highlights in Figure 1. The diagram in Figure 2 shows how the elements interact.
Clearly the code in Figure 3 could be a little tighter. It was intended to apply it to both translation and rotation elements of the block movement. I forgot to add it to rotation, as will be obvious from the listing. I have not yet seen it affect the game play, but it should!



At this point the reference point for the shape is updated to reflect the location change, if one has been permitted. Now that the block is in its new position two further checks are made, that the shape is within the play area and whether a contact has occurred between the current shape and any existing one or the end of the playing area.
Play Area Limits
GetFaceCentroids, Figure 4, checks that the whole shape is within the limits of the playing area. This is more conveniently done after the instances are translated. Again, this function is shared (it really is this time) with the rotation function. Again, the method is low-tech, simply sorting all the coordinates into two lists by X-coordinate and Y-coordinate, yellow Figure 4, and then comparing these to the limiting factors. It is only necessary to check the extreme ends of the lists, since these represent the biggest deviations. If a point is found outside the limits, then a translation vector is created and returned that will correct it. Note that the vector is 3D, initially just (0,0,0) and has the X and Y components changed separately. It will only be used if the flag bTranslate is set to True.

Contact Checking
The final check is to see if any contacts have been made between the current block and either the end of the play area or any existing blocks, that prevents the block travelling further down the track. This is much more involved function because it must check to determine if a contact has been made and if so where in the play area. If the contact has happened at the head of the playing area it must end the game, if lower down then the list of contact surfaces to be checked must be updated to include those in the old shape, which is now static. Once this list is updated then a test of whether a scoring event has happened can be applied, and then a new shape created. No contacts being found just passes through without action.

The contact check method is very similar to the method used to find the limits above, purple, Figure 5. It tests if the unit normal of the moving cube face points down the track, i.e. the unit normal has an X component = 1.0, and if the face centroid appears in the contacts list of centroids. The contacts list contains the centroids of the existing cube faces that point up the track, i.e. their unit normal X component is -1.0. The list of centroids also contains the centroids of the subdivided end wall faces, see Part 3.
If no contact is found, nothing happens and the game continues. If a contact has been found, then CheckTop is used, Figure 6, to find whether the current block as stopped at the top of the track. It simply checks the X-coordinate of the cube face centroids and if the minimum value is 1.0 or less the shape is sitting at the head of the play area. This means that the stack has built up and the game is over, calling the GameOver function. If the game is not over, then preparation must be made for the game to continue.

Game Over
The GameOver function, Figure 7, displays the annotation indicating the game is over and calls HighScores, Figure 8, which displays an annotation which lists all the current high scores, and adds the current score to this list if it makes the grade. One important thing to note with annotations is that if you wish any changes to them to be displayed immediately, within the execution of the current function, then you must force the display to refresh, purple highlight, Figure 7.

Creating the score board annotation was a little more involved because it has to store the previous high score values and also format the output in a neat way. The first part is best handled using a text file stored with the plugin. Reading the existing list from this file, inserting the current score and writing back only the first nine values is a simple way to maintain the list. Nine values were used because I didn’t pad the position number to two digits as I have the score, top ten would have been neater!
Formatting the annotation correctly required a good choice of font. To get the line padding to work a fixed width font was required, in this case Consolas, see Part 3. If a variable width font is used the lines may contain the same number of characters but they will be of different lengths. It also required forcing the score value to appear at a fixed width of 8 digits, padded with leading zeros.

Game Continues
If the game is continuing then the current shape’s collection of cubes must have their face’s centroids collected, to control location limits and define contacts, this is done in AddContacts, Figure 9. The yellow section in Figure 9 assigns the centroids of faces, defined by their unit normal properties, to the controlling reference lists for contact and limits.
The green section is important, it converts the current location constraints on the instances to fixed locations in space. Failure to do this can have strange results.

Now, whether one or more rows has been completed must be tested and the score updated, see CheckScore, Figure 10. This is done by sorting the contacts list by X coordinate and then checking in blocks of nine that the X coordinate is the same. If it is then the row is complete and must be deleted, by removing the appropriate instances and the score updated. The rows before this need to be moved down the track and the search repeated from the top. This requires that the lists of centroids that define the contacts and position limits are updated to remove the old information and reflect the new positions of their parent parts.
This section exposes that a little more planning and less growing the example as I went along would have paid off. I should have worked out a proper data structure that naturally kept the contact data and instance names together, rather than using the rather heavy handed sorting method in OrganiseSets, Figure 11, and RebuildSets, Figure 12.



Conclusion
This is how I implemented the movement, contact and scoring in my version of TETRIS. It is a little rough and ready, there is certainly room for improvement. However, it is primarily a demonstration of scripting techniques within ABAQUS, dressed up as a little light entertainment. I might even enclose it as an Easter egg in future projects just to encourage users to dig through the options!



Comments