How I made Tetris with Pygame
I made a bet with my father: that I could make the classic arcade game of Tetris in under an hour, using the programming language Python. In the end, it took me much longer than that. Surprisingly, there are many nuances to the game, so I am going to explain them all in this blog post!
This blog post will primarily focus on the concepts behind making the game of Tetris, rather than on the actual code. Some snippets of the code will be added in here and there though!
Briefing: The Rules of Tetris
Almost everyone in the digital age has at least heard of Tetris. It’s a classic arcade game in which four-unit blocks that come in different shapes fall down a grid. The job of the player is to fit the blocks together, by shifting them horizontally while simultaneously rotating them. If a full row is completed, the player is awarded points and the row is cleared from the grid.
If you look at the image on the left, you’ll see the different types of tetrominos (the official term for a four block unit). There are a total of seven types of tetrominos. We’ll go over these later on in the post!
I’d suggest that you play the game a couple times if you aren’t already familiar with it. You can play it for free here. It’s really addicting and time consuming though, so don’t forget to come back to the blog post!
Part 1: Creating a Tetromino Bag
Tetris has seven different types of tetrominos: I, J, L, O, S, T, and Z. The image above shows these blocks in the same ordering as the letters above.
Tetris is a game about chance and skill. Most of the skill involves maximizing the chances of having good luck by putting yourself in an probabilistically advantageous position, similar to games such as Poker or Backgammon. Most of the strategy involves arranging the blocks to complete filled rows while maximizing the chances of good placement of future random blocks. In order to make sure that players get a somewhat even distribution of blocks, Tetris spawns blocks semi-randomly, rather than completely randomly. There is a very good article on the different types of Tetris block randomizers here if you are interested, but for the purposes of our game, we will be using a “14-bag” system.
Poker utilizes a standard deck of 52 cards. There is only one copy of each card in the deck (unless someone at the table is a cheater!), meaning that if you draw a 2♥️, no one will be able to draw another 2♥️. The 14-bag system utilizes a similar principle, which ensures that you don’t get too many of the same block.
Imagine that you had two of each tetromino shown in the image to the left, as well as the empty bag. Now, if you put all of your tetrominos in that bag, you’ll have a bag with 14 tetrominos, which is why it is called a “14-bag.” Each time you need a new tetromino, you randomly draw one from that bag, until you run out. Once you run out, you replace the whole bag using the same process as before! This is similar to drawing from a deck of cards and reshuffling everything once all of the cards are drawn. Using this system ensures that you don’t get too many of a certain type of block.
The code below is what actually executes this process for us on the computer. It makes a tetromino bag for us, and adds two of each tetromino type into the bag with MakeDoubleBag(). Whenever we want to use a tetromino from this bag, we use Choose(), which selects a block for us and removes it from the bag. However, Choose() first checks if the bag is empty (since it is impossible to remove something from an empty bag!) and runs MakeDoubleBag() if it is, which puts 14 new tetrominos into the bag.
Part 2: Putting Tetrominos on a Tetris Grid and Letting them Fall
Now that we have gotten through the process of choosing tetrominos, we need to put those tetrominos somewhere! We will drop tetrominos one at a time from the top of the screen. For the sake of clarity, the tetromino that is falling at the moment of reference will be called the “active tetromino” or the “falling tetromino.” When the falling tetromino touches the bottom of the screen it will become settled, and a new tetromino chosen from the tetromino bag will take its place as the active tetromino. However, if the falling tetromino touches a settled tetromino before reaching the bottom, it will stop and settle right on top of the settled tetromino.
How are we going to implement all of this into the program though?
Tetris uses a grid system, and typically is 10 by 20 blocks. In comparison, Tic Tac Toe is typically 3 by 3 blocks. When you draw the four intersecting lines of Tic Tac Toe, what you are essentially doing is creating a 3 by 3 grid. In mathematics, this is often called a “2D matrix.” Essentially, a 2D matrix holds data in two dimensions, which can be referenced with indexes. For example, the value of (3, 3) on this board would be an X, since there is an X on the intersection between the third row and the third column. In general, (x, y) of a 2D matrix should return the value at the intersection between the xth column and the yth row. Following this logic, the value of (1, 3) of this 2D matrix would be O, and the value of (3, 2) would be nothing. In computer science, when you retrieve a value from a larger, sequential dataset using its location in the dataset (as we are doing right now), you are “indexing” that larger dataset.
We will use a 2D matrix to represent the values of the Tetris board. This board will be 10 by 20, as mentioned above. Instead of X’s and O’s, like in Tic Tac Toe, we’ll be using 0s, 1s, and 2s.
0 = Empty Spot!
1= Settled Block!
2= Falling Block! (part of the active tetromino)
The gif to the left visualizes the process of blocks dropping. As you can see, using a 2D matrix to represent our Tetris game is quite handy!
The way that the individual tetrominos are categorized are also as 2D matrixes. Each of these tetrominos have matrixes just like the main Tetris board, where there are empty blocks and filled blocks. The reason why keeping the empty blocks will become important later, when we get around to rotating the tetrominos. The code below shows how the 2D matrix for the active tetromino is initialized. If you look at the shapes formed from the 2s in the matrixes, you can actually see how they form the shapes of the tetrominos.
Moreover, these tetromino matrixes also have a coordinate pair attached. This isn’t shown in the code, but basically, for the active tetromino, there are two essential data pieces: the matrix with the shape of the tetromino (which is shown above), and a coordinate tuple telling the computer where the top left of the 2D matrix should be on the main Tetris grid. In order to “drop” the tetrominos, the y of the coordinate pair is increased by 1 over and over again. In Pygame, the origin coordinate (0, 0) is at the top left corner of the screen, so increasing the y coordinate actually means you are going downwards (it’s weird, I know). This makes it easy to drop every single one of the four blocks in a tetromino just by changing 1 value! Each time it is increased, the program checks if it has reached the bottom of the Tetris grid or if any of its blocks are touching a settled block. If any of these conditions are true, the tetromino settles and a new active tetromino is initialized. Finally, if the tetromino reaches the top row of the screen, the game is concluded.
Part 3: Rendering
We will be using Pygame to render everything. Pygame is a library for Python which allows users to create a game window where shapes, text, and images can be rendered. I’m not going to go into detail about the technicalities of Pygame.
For this piece of code, grid.matrix refers to the main Tetris grid. Screen.screen is simply the canvas where everything is drawn to. Essentially, what this code does is it loops through every single value in grid.matrix, checks if the value is not empty (not equal to 0), and renders a block if so. The screen is being rendered many times a second, and each time the screen is being rendered, this code runs to draw all the blocks.
There are other parts of the rendering process that I will leave out for the sake of simplicity. I am leaving out how I added the user score text later on (look into pygame.freetype if you want to learn more), and other minuscule details I added for the sake of visual clarity.
Part 4: Moving Tetrominos Left and Right
If you remember from Part 2, the active tetromino has two data components: a matrix with the “shape” of the tetromino and a coordinate tuple with the “position” of the tetromino. This is super important to remember, as this is fundamental to many processes in the game. Having one component wouldn’t be sufficient: if I just tell you to draw a smiley face on a paper, but I don’t tell you where exactly to draw it, then you might draw it in a spot I didn’t want! On the other hand, if I tell you where to draw something, but I don’t specify what to draw, then you have nothing to draw!
For the example shown in the beautiful figure above, the smiley face is equivalent to our 2D matrix (the shape) and the location is equivalent to our coordinate tuple (the position). The alternative system to this would be to only use the main 2D matrix, which saves us from the hassle of transcribing data. However, this turns out to waste a lot more time in the long run, for both the programmer and the computer. You see, each line of code that the computer has to perform takes some time, and although it may seem minuscule and negligible at first, since computers are so fast nowadays, many of these actions can accumulate, resulting in poor performance. For a game, this is especially important, because you don’t want for your game to be laggy.
With our first system, whenever we want to move anything, we only have to change a coordinate in our coordinate tuple, which is super simple! For the second system, you have to loop through all of the items in the 2D matrix, check for the ones that have a value of 2 (which denotes it is part of the active tetromino), and then shift it over. This gets super annoying, trust me. Hopefully, the gif to the left can convince you that iteration through all 200 potential blocks of the grid just for 4 actual blocks is very inefficient.
Alright, now that we know what to do to move the tetromino left or right, we need a way to actually input when we want to go left or right! Luckily, pygame has some key detection functionality.
Line 3 of this code gets a list of all the pressed keys at the current moment. The if statements that come after check if the RIGHT or LEFT keys are pressed, and move the tetromino in the corresponding direction if they are. Of course, this is a massive oversimplification of the whole process, since you also have to do a bunch of collision detection to make sure that your tetromino doesn’t shift out of the grid or overlap with settled blocks. I also threw in some code later on (not shown) that lets the user drop the tetromino quicker, using the same principle described by this section, but instead of with the x coordinate and the LEFT and RIGHT keys, I use the DOWN key and modify the y coordinate.
Part 5: Rotating Tetrominos
Since we conveniently have our active tetromino data organized into a n by n matrix and a coordinate tuple (refer back to the code from part 2), rotating the tetromino should be easy. All we have to do is use a pre-existing function to rotate a matrix, such as numpy.rot90(). What this does is pretty simple. Imagine that you can hold the matrix in your hands, in the form of a board. If you turn it to the left or to the right, what you have essentially done is rotate the matrix, just like the numpy.rot90() function.
None of this would’ve worked if we used the second system mentioned in Part 4, since that would rotate the ENTIRE board! Anyways, just like how we used key inputs for horizontal movement, we will do the same for rotations. Q will rotate the active tetromino counterclockwise, and W will rotate the active tetromino clockwise.
To be honest, rotations were the most challenging part of doing this whole project for me. I originally had the active tetromino data solely in the main 2D matrix, so after struggling for a long time to come up with a way to rotate the tetromino without rotating the entire matrix, I came up with the shape/position system that I’ve talked so dearly about in this post. Moreover, I had to deal with rotations that went off the screen (which I fixed using “wallkicks”) and I had to write code to prevent rotations that would make the active tetromino overlap with settled blocks.
Part 6: Scoring Completed Rows
In order to check if a row is completed, every time an active tetromino settles, the game iterates through each row. If there are no values in that row that are equal to 0, then the row is cleared, points are awarded, and anything above falls down one space. The gif to the left shows how row detection works. All of the rows highlighted in green have been recognized as completed rows (the completed rows are highlighted green once they have been checked as completely full by the program).
In an optimized algorithm, you could skip the entire row once you detect a single 0, since that means that the row is not completed. This gif doesn’t do that though, just because it looks cooler to loop through the whole thing.
Part 7: Adding a Twist to the Game! (Gerrymandering)
I decided to add a twist of “gerrymandering” to my game.
Gerrymandering is a technique in which district borders are manipulated in order to exploit the “majority takes all” system, in which the majority in a district will take all the voting points. By getting a slight majority in districts, many politicians are essentially able to mitigate opposing votes from those districts will taking all the points for themselves. The image below shows how the ability to draw district borders allows for gerrymandering to occur.
I decided to deploy this concept in my game. If you can get more orange than blue in a row, you get triple the amount of points! This adds another layer of complexity to the game, since the player needs to get a majority of orange in each row, without the majority being too overwhelming, since that would be a waste of precious orange blocks. While this isn’t the same exact thing as gerrymandering, since you aren’t changing the borders of districts, it is similar since you are positioning demographics in order to get slight majorities.
In order to do this, I had to add color into my game. In normal Tetris, color is nothing but a visual aid, but for this game, color is an important part of the gameplay. I alternate the dropped tetromino colors between blue and orange in order to maintain an even distribution at all times. I stored the values of the colors of each block in a separate 2D matrix, which I called “colormatrix.” Anyways, that’s too complicated to dive into, so we’ll stop at that!
Part 8: Instructions and End Screen
This stuff is all miscellaneous, so I won’t go too into the details. I added some instructions before the game starts, and an ending screen for when the game ends.
Part 9: Final Game
There are a bunch of things I added into my game that I didn’t discuss in this blog post. However, this post encompasses the most essential parts of my game. I hope you enjoyed reading it!
The final game is embedded below. To play, all you need to do is press “Run,” wait until the game screen pops up, and then click on it. There is an instruction screen afterwards, read it carefully!
Notice: You might notice some display and input lag. This isn’t because of the code. This is because the game is being run on repl.it, a service that allows for Python code to be run on browsers. Pygame is meant to be run on the computer, rather than in a browser, so I have to use this service as a workaround. The downside of this service is that things run slower, so bear with me.
If the embed isn’t working, then go to this link: https://repl.it/@AvenuesCC/Pygame-Classic-Arcade-Game-DanielChuang1.