Cloning the Classics: Tetris

November 3, 2021

Tetris clone image

About a year and a half ago I built a Tetris clone as part of my “Cloning the Classics” series. I had some fun and learned while building it but never got around to writing about it which is one of the reasons why I have this website. So here goes.

When I started the project, years have passed since I played the game. Many of the details and rules were fuzzy, but lucky me, The Tetris Company maintains a Tetris Guideline meant to help game developers standardize Tetris gameplay across all versions of the game. It’s a guide for how the game should look visually and how it should play (timings, rules, features, etc). The guidelines were my requirements for development and going through the document clarified many of the things about the game I forgotten or flat out never knew.

For instance, what do you think the fall speed for a Tetrimino at level 3 is? If you know, you are amazing. If not, don’t worry, I didn’t know either. It is 0.617796 lines per second. There is an equation for it if you are ever in doubt:

(0.8 - ((level - 1) * 0.007)) (level-1)

Or, do you know how many points a player can earn for a back-to-back T-Spin double followed by a T-Spin triple in level 1? Someone out there probably knows this. But if you don’t, it’s 5400 points.

I wouldn’t have known these details as a casual player but both can be found (along with many more) in the 2009 guidelines [1] , pages 20 and 21, respectively.

Once I understood what I was building, I dedicated up to an hour on sporadic weeknights for three weeks to build the clone. I could have continued to finish more requirements, but at a certain point I decided I got what I wanted out of the exercise (learn and have fun!).

Many of the basic requirements are complete with features most people would recognize as classic Tetris: shapes, rotations, soft drops, hard drops, locking and the next queue. There were some features I just didn’t get to to like the ghost piece feature which previews where a piece would fall. There is a quite a large section explaining T-spins in various scenarios, three pages worth to be exact and I probably didn’t get all the possibilities ironed out in my version.

While building the game, I learned, among other things, how to rotate a 2D list which is probably something I should have known already, but hey, it’s never too late to learn. Anyways, a Tetrimino was modeled using either a 3x3 or 4x4 list and they need to rotate either clockwise or counter-clockwise. With some Googling, Python’s zip, reversed and variadic parameters proved useful.

Let’s say we have a T shape. With 1’s representing a block of space occupied by the shape and 0’s representing a block of empty space. The T shape can be modeled in its default orientation like so (yes, the T is upside down, but per the Guidelines, this is the default orientation):

|0|0|0|
|0|1|0|
|1|1|1|

If we break it down into a 2D list [2] :

shape = [[0, 0, 0],[0, 1, 0], [1, 1, 1]]

We can reverse the list:

reversed_shape = [[1, 1, 1], [0, 1, 0], [0, 0, 0]]

Then we can zip the reversed list:

zipped_shape = [[1, 0, 0], [1, 1, 0], [1, 0, 0]]

When you zip a set of lists, a new list of lists is created using the values at the matching indexes of the child lists. For instance, the values at index 0 in list 1, list 2, and list 3 are combined into a new list. The values at index 1 in list 1, list 2 and list 3 are combined into another list. And so on.

With that, the final shape is rotated clockwise and looks like this on the 2D grid:

|1|0|0|
|1|1|0|
|1|0|0|

In Python, we can combine these steps into a pipeline of functions and get a nice one liner:

rotated_shape = list(zip(*reversed(shape)))

zip takes a list of iterators, so we use the * operator to unpack the reversed list of lists into individual lists.

A rotation is not guaranteed because a wall or locked Tetrimino can block a rotation. We have to check during the frame whether the rotation is valid by using a temporary proposed shape variable (shape_attempt). The proposed rotation is mapped and checked against the playing field for any collisions. If any collisions are detected, the rotation fails and the player is notified with a failure sound.

def rotate_clockwise(self):
    shape_attempt = list(zip(*reversed(self.shape)))
    if not self.is_collision_on_rotate(shape_attempt):
        self.shape = shape_attempt
        arcade.play_sound(self.rotate_sound)
    else:
        arcade.play_sound(self.rotate_fail)

def rotate_counter_clockwise(self):
    shape_attempt = list(reversed(list(zip(*self.shape))))
    if not self.is_collision_on_rotate(shape_attempt):
        self.shape = shape_attempt
        arcade.play_sound(self.rotate_sound)
    else:
        arcade.play_sound(self.rotate_fail)

We have to temporarily overlay the proposed shape over the playing field to check for collisions. The game world is represented by a larger 2D list where 0’s represent empty space and non-zero integers represent occupied space. The integers work double duty as they also represent the color that needs to be rendered on the grid.

Once we know where the shape is trying to go, we can iterate through the proposed shape and add the values between it and the section of the playing field it is trying to occupy. If the combined value at each coordinate is not equal to either the color value of the shape or the original value from the playing field, there is a collision and the rotation should fail.

def is_collision_on_rotate(self, new_shape):
  for i, row in enumerate(reversed(new_shape)):
      for j, column in enumerate(row):
          matrix_value = self._grid[self.y + i][self.x + j]
          col_sum = matrix_value + row[j]
          if col_sum != self.color and col_sum != matrix_value:
              return True
  return False

An example might help. Lets say we have two vertical I shapes and they are represented by the integer 3:

|0|3|0|3|
|0|3|0|3|
|0|3|0|3|
|0|3|0|3|

If we try to rotate the left instance, we get:

|0|0|0|3|
|3|3|3|6|
|0|0|0|3|
|0|0|0|3|

We can see where the collision would occur. The value 6 does not equal the color value of the shape (3) and does not equal the original game world value (also 3). This would fail the collision check and the player would have to try to make another move.

And there you have it, the goal was to learn something and possibly share something while having a little fun. Although the project is not “feature complete”, it was a success based on the goals I had for myself. And, writing it down and sharing helped me collect my thoughts and put my self in a uncomfortable position. Perfect.

If you are curious to see the rest of the code for this project, check it out here.

Notes


01: 2009 Tetris Design Guideline (Dropbox link)

02: Some of you may be wondering whether the ordering in the 2D list is correct and you would be right to wonder. Depending on where the origin lies in the game, you will have to process the list differently. In my T shape example, the coordinate (0, 0) falls to the top left of the representation; or, in other words, the first index of the first list. Most game engines use the bottom left as (0, 0). I would have to reverse the final shape before comparing it to the game world which is what I eventually do in the game implementation in is_collision_on_rotate.