In the previous posts, I once analyzed a Tetris game developed in Scratch. It uses Pen drawing tab to draw all the tetris pieces. However, due to the limitation of data structure in Scratch, the code structure is super complicated compared to other Scratch projects.
How about we implement it in Python?
On YouTube, some videos gave step-by-step code guidance on developing Python games. In the following video, the author introduced five Python projects. One of them is the tetris game. If you are interested, you could open YouTube to watch and even download the code.
However, the original tetris code in the above video is a bit complicated, so Python starters might feel confused. Those difficult points include:
a. The code uses grid to store all the position’s color information, including the current falling tetris piece. Since the existing pieces and current piece information is mixed together, the program uses a dictionary called “locked_positions” to store the positions of existing pieces. For each iteration of the main loop, the program needs to load data from locked_positions dictionary and convert data format to fill grid.
b. In the nested list “grid” (it is created in main() function), its first dimension represents row information, and the second dimension represents column information. This definition is reasonal, because when the program tries to clear one row, such a structure makes it convenient to delete one row’s data and then insert a new row.
However, the “locked_positions” dictionary’s key (it stores row and column index of the tile) is arranged in the tuple structure of (column index, row index).
c. Due to the inconsistancy of data storage structure, the program needs to convert the data format in order to check if tile position is valid (in valid_space function) or if one row needs to be cleared (in clear_row method). It is troublesome.
d. The program defines a class but it only contains one constructor method. Except this, all the other functionalities are realized by functions. It is a good idea to redesign the code structure to utilize OOP method.
In this post, I will introduced an updated design which makes it easier and clearer to understand the whole process. So let’s start now!
The beginning of the program does not change a lot. I just changed those global variables into capital letters to indicate that they are constants. I also added two variables “COL_COUNT” and “ROW_COUNT” because several functions use them.
From line 25 to line 125, all the code keeps unchanged from the orignal version. Those nested lists define the tile position of each shape at different directions. Some lists contain four nested lists, some contain two nested lists, and others contain one. It is becuase when shapes rotate, their position information gets changed. For example, shape “I”, “S” and “Z” have two formats. Shape “T”, “J” and “L” have four formats, while shape “O” has only one format because no matter which direction it rotates to, it always shows the same shape.
At line 128, the program defines list called “shape_list” which again nests all the above defined seven shapes. At line 130, it defines “shape_color” list which contains seven RGB color tuples, representing different colors of each shape.
From line 134, the program starts defining a class called “Piece”. You might notice that the class inherits from base class “object”. Actually, if you are using Python 3.x, there is no difference if Piece class inherits from object or not. In Python 3.x, all the defined class inherits from base class “object” by default.
In the __init__ constructor, the program defines more attributes than that in the original version. Please note that I changes attribute self.y and self.x into self.col and self.row respectively. The attribute self.tile_pos is added to store falling piece’s tile positions. In the constructor method, its value is None.
From line 144 to 161, the program defines method “get_piece_tile_pos”. This method is converted from the original function “convert_shape_format”. What it does is to update falling piece’s position information based on piece’s shape, direction, row and col values. In order to keep consistent with the structure of the grid, “positions” list will also follow the structure of using (row index, column index). It is different from the original version. This change is important since it does not need data format conversion.
The following diagram illustrates how to get current piece’s position based on current piece’s row and column value. Please note that current piece’s row and column value is indicated by the plus sign. Therefore, in order to “translate” the shape list information into position, the program needs to offset row and column value by using (self.row – 4) and (self.col – 2) to get proper position values.
The above method “get_piece_tile_pos” is called by the method “is_valid_pos” at line 165. “is_valid_pos” method is renamed from the orginal function “valid_place”. At line 166, the program gets all the positions which are not occupied by the existing pieces. At line 168, the program changes the 2-dimensional list into a 1-dimensional list by using list comprehension to flatten it. If you are not familiar with the syntax of list comprehension, you might refer to some online material.
From line 170 to 174, the program checks each position stored in self.tile_pos. If any position information does not belong to the accepted_positions, it means the falling piece’s position is invalid. This invalidity might result from conflicing positions with existing pieces on the board or falling outside of the board. The method returns True if all the position information is valid, otherwise, return False.
For the method “draw_next_piece”, it draws the text “Next Shape” and the next piece to the right of play window. Its logic is different from drawing the falling tetris piece. I put this method in the class Piece because it represents another format of Piece object.
That is end of the Piece class definition. The following code defines some functions used by the main() function.
At line 201, the program defines functions “check_lost”. It checks if the game is over. The rule is that if any position in the first row (top row) is occupied, it means the tile has piled up to the ceiling of the winodw, so the game is over.
From line 209 to line 222, the program defines two functions “draw_text_middle” and “draw_score”. The two funtions are similar. They just define font and blit them to the main surface of the game.
From line 225 to line 235, the program draws the grid lines by using pygame.draw.line method. This part of functionality is originally from “draw_window” function. I extracted it from the original function, so that the rendering could be more flexible.
Line 238 to line 262, the program defines an important function called “clear_rows”. This one contains a bit algorithm to clear a row and rearrange the left rows. The program starts from the bottom row, whose row index is Row_Count – 1. It iterates each position – grid[row_index][col_index] on this row. If any position’s RGB value is (0, 0, 0), it means the row is not fully occupied, so the program just exits from the nested loop and set variable “clear” to False.
If “clear” variable is True, the program deletes the row in grid. After that, it inserts a new row at the index 0 position of grid and then fills (0, 0, 0) for each column position. That means, the gird is inserted an empty new row. Meanwhile, since the row at row_index has been deleted, the above row falls onto the row at row_index, so the program still needs to check position RGB value at row_index. However, if clear is False, the row_index decreases by 1 and the program checks the following rows.
When the row_index is equal to inc, it means the program has already checked all of the existing rows. You might ask that the program needs to check till row_index = 0. However, remember there is cleared rows and so some new rows are inserted to the starting place of the grid. When row_index is equal to inc, it means all the previous old rows have been checked.
From line 265 to line 280, the program defines another function “draw_existing_tiles”. It just draws those small rectangles, representing the existing pieces. This function is also extracted from the original function “draw_window” to make rendering more flexible.
That is part one of the whole project. It contains the Piece class definition and all the functions. In the next part, we will define main function and enter the main loop of the game. Stay tuned and enjoy the coding!
Note: All the analysis articles are copyright products of http://www.thecodingfun.com. Anyone re-posting them should credit author and original source. Anyone using them for commercial purposes or translating them into other languages should notify TheCodingFun and get confirmation first. All Rights Reserved.