top of page

Post #15 TETRIS in ABAQUS – Part 4

Jul 27
8 min read

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!


Figure 1 TranslateShape - function translates a group of cubes that represent the current playing piece.
Figure 1 TranslateShape - function translates a group of cubes that represent the current playing piece.
Figure 2 Using the Y-face normals to determine if the moving block is permitted to move up or down. In this case upward movement is accepted but downward movement is blocked by the detection of the matched centroids highlighted.
Figure 2 Using the Y-face normals to determine if the moving block is permitted to move up or down. In this case upward movement is accepted but downward movement is blocked by the detection of the matched centroids highlighted.
Figure 3 CheckLimits - comparing the face positions of the current cubes with a list of existing cubes and where appropriate returning a direction flag.
Figure 3 CheckLimits - comparing the face positions of the current cubes with a list of existing cubes and where appropriate returning a direction flag.

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.


Figure 4 Checking all the face centroids in the current shape to see that they are all in play. Only need to check the ends of each list to see if they exceed and create a translation that respects the limits if they do.
Figure 4 Checking all the face centroids in the current shape to see that they are all in play. Only need to check the ends of each list to see if they exceed and create a translation that respects the limits if they do.

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.


Figure 5 Contact checking. Similar method to earlier, the cube faces that point down the track are checked to see if their centroids are in the list of existing block or end wall face centroids. If so, then a contact is registered. Further processing is then done to determine the location of the contact and either end the game or check if a scoring event has occurred before continuing with a new block.
Figure 5 Contact checking. Similar method to earlier, the cube faces that point down the track are checked to see if their centroids are in the list of existing block or end wall face centroids. If so, then a contact is registered. Further processing is then done to determine the location of the contact and either end the game or check if a scoring event has occurred before continuing with a new block.

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.


Figure 6 CheckTop finds the minimum X coordinate of the face centroids, a value less than 1 indicates the block is at the top of the play area.
Figure 6 CheckTop finds the minimum X coordinate of the face centroids, a value less than 1 indicates the block is at the top of the play area.

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.


Figure 7 GameOver displays the annotation showing the game over sign and also the high score board.
Figure 7 GameOver displays the annotation showing the game over sign and also the high score board.

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.


Figure 8 Creating the scoreboard. Only the top nine values will be kept, with the current score inserted if appropriate. Note the use of the getInput dialog to collect the player’s name
Figure 8 Creating the scoreboard. Only the top nine values will be kept, with the current score inserted if appropriate. Note the use of the getInput dialog to collect the player’s name

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.


Figure 9 Adding the current collection of cube's data to the reference lists that control location and contact.
Figure 9 Adding the current collection of cube's data to the reference lists that control location and contact.

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.


Figure 10 Checking for completed rows. Note the important role of OrganiseSets and RebuildSets in keeping all the data together. This could be better done with more thought applied to a suitable data structure!
Figure 10 Checking for completed rows. Note the important role of OrganiseSets and RebuildSets in keeping all the data together. This could be better done with more thought applied to a suitable data structure!
Figure 11 Sorting the contacts and names data by the contact coordinates.
Figure 11 Sorting the contacts and names data by the contact coordinates.
Figure 12 Rebuilding the contact and name sets when a row has been deleted.
Figure 12 Rebuilding the contact and name sets when a row has been deleted.


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


bottom of page