[EN] Simple Tetris Ep.1

This article introduces how to write a simple Tetris game by displaying it in a grid of 10 widths and a height of 16 as shown in Figure 1. Using esp32 microcontroller board connected to ST7735 display and 8 switches for controlling. Importantly it is written in Python via MicroPython compiled using the st7735_mpy library. In this article, we talk about storing 7 types of objects that fall, to support the display and rotation of objects with moving objects left and right. The controls and logic of the Tetris game will be discussed in the next article.

Figure 1 Sample game in this article

Data structure

Tetris games contain the main information that must be stored are all 7 falling objects as shown in Figures 1 to 8, which are found to be stored in the form of a 4×4 grid. If any position is part of the object, it will keep the value to 1, but if any position is not the object, it will keep its value to 0, where the data structure of each object is as follows.

Object 1

The first object is a long bar with 4 squares, 1 square high as in Figure 2, and when pressed it turns into a bar 4 squares high and 1 wide.

Figure 2 Object 1

Storage of object 1 to support two types of rotations as follows:

    [ # 1
     [[1,1,1,1], # rotate=0
      [0,0,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,0,0,0],
      [1,0,0,0],
      [1,0,0,0]]
    ]

Object 2

The second object is as shown in Figure 3, can be rotated in 4 ways and results in the size of the object being changed to be 2×3, 3×2, 2×3 and 3×2 as shown in Figure 3.

Figure 3 Object 2

การจัดเก็บข้อมูลของวัตถุที่ 2 เพื่อให้รองรับการหมุนได้ 4 แบบ เป็นดังนี้

    [ # 2
     [[1,1,1,0], # rotate=0
      [1,0,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=1
      [0,1,0,0],
      [0,1,0,0],
      [0,0,0,0]],
     [[0,0,1,0], # rotate=2
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=3
      [1,0,0,0],
      [1,0,0,0],
      [0,0,0,0]]
    ]

Object 3

Objects of this type are similar to Type 2, but they are positioned opposite each other as in Figure 4.

Figure 4 Object 3

Storage of the 3rd object to support 4 types of rotations as follows:

    [ # 3
     [[1,0,0,0], # rotate=0
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=1
      [1,0,0,0],
      [1,0,0,0],
      [0,0,0,0]],
     [[1,1,1,0], # rotate=2
      [0,0,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=3
      [0,1,0,0],
      [1,1,0,0],
      [0,0,0,0]]
    ]

Object 4

The fourth object is a three-square bar with a central protrusion as shown in Figure 5, making the object dimensions when rotated at different angles to be 2×3, 3×2, 2×3 and 3×2.

Figure 5 Object 4

Storage of the 4th object to support 4 rotations as follows:

    [ #  4
     [[0,1,0,0], # rotate=0
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,1,0,0],
      [1,0,0,0],
      [0,0,0,0]],
     [[1,1,1,0], # rotate=2
      [0,1,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=3
      [1,1,0,0],
      [0,1,0,0],
      [0,0,0,0]],
    ]

Object 5

Type 5 object has dimensions 2×3 and when rotated it will become 3×2 as shown in Figure 6.

Figure 6 Object 5

การจัดเก็บข้อมูลของวัตถุที่ 5 เพื่อให้รองรับการหมุนได้ 2 แบบ เป็นดังนี้

    [ # 5
     [[0,1,1,0], # rotate=0
      [1,1,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,1,0,0],
      [0,1,0,0],
      [0,0,0,0]]
    ]

Object 6

Type 6 object is opposite object Type 5 as shown in Figure 7.

Figure 7 Object 6

Storage of the 6th object to support two types of rotation:

    [ #  6
     [[1,1,0,0], # rotate=0
      [0,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=1
      [1,1,0,0],
      [1,0,0,0],
      [0,0,0,0]]
    ]

Object 7

Objects of this type are 2×2, like a square box, so they can be rotated in any way to keep their original shape. Therefore, there is no need to rotate the object as shown in Figure 8.

Figure 8 Object 7

The storage of the 7th object is as follows.

    [ # 7
     [[1,1,0,0], # rotate=0
      [1,1,0,0],
      [0,0,0,0],
      [0,0,0,0]]
    ]

Movement

The input device consists of 8 switches that are connected in a pull-up way. The name is shown in Figures 9 and 10.

Figure 9 A,B switches
Figure 10 M1 and M2 switches

The configuration for the 8 switches is as follows.

keL = Pin(39, Pin.IN, Pin.PULL_UP)
keU = Pin(34, Pin.IN, Pin.PULL_UP)
keD = Pin(35, Pin.IN, Pin.PULL_UP)
keR = Pin(32, Pin.IN, Pin.PULL_UP)

swM1 = Pin(33, Pin.IN, Pin.PULL_UP)
swM2 = Pin(25, Pin.IN, Pin.PULL_UP)
swA = Pin(26, Pin.IN, Pin.PULL_UP)
swB = Pin(27, Pin.IN, Pin.PULL_UP)

Random the object

Objects are randomized from the class random, with a value range of 0 to 6 representing objects types 1 to 7. The behavior of randomization when swM2 is pressed is as follows:

        actorNo = random.randint(0,len(actors)-1)

Shift to the left

This is because every object has a starting position in the upper left corner. This makes it possible to scroll to the left by decreasing posX unless it is already on the edge of the screen and not decreasing the position as follows:

        if posX > 0:
            table()
            posX -= 1
            draw()

Shifting to the right of an object of type 1

The working principle of moving an object to the right relies on the values posX and actorRotate as variables for determining the scroll:

  • If actorRotate is 0, the object is 4 squares in length, so if the space to the right isn’t large enough, it won’t move. But if it’s enough to move to the right by increasing the posX value.
  • If actorRotate is 1, then the width of the object is 1, then if the object is not aligned to the right pane, posX is incremented.

The code is as follows.

            if (actorRotate == 0):
                maxX = maxCol-4
            else:
                maxX = maxCol-1
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of type 2 objects.

The working principle of moving an object to the right of object type 2 relies on the values posX and actorRotate as variables for determining the scroll:

  • If actorRotate is 0, the object is 3 squares in length, so if the space to the right isn’t large enough, it won’t move. But if it’s enough to move to the right by increasing the posX value.
  • If actorRotate is 1, then the object’s width is 2, then if the object is not aligned to the right pane, posX is incremented.
  • If actorRotate is 2, this makes the object 2 spaces wide, allowing it to be moved to the right if there is still space to the right.
  • If actorRotate is 3 then case 2.

Code

            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of an object of type 3

The working principle of moving object to the right of object type 3 is the same as type 2, the code of operation is as follows.

            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of an object of type 4

Moving a Type 4 object to the right has the same principle as an object Type 2 and 3.

            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of type 5 objects

Moving a Type 5 object to the right determines whether there is enough space to the right, i.e., in type 0 rotation, it must have 3 units of the area; in Type 1, it must have 2 with the following code

            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of type 6 objects

Movement of object type 6 has the same principle as object type 6 as follows:

            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()

Shifting to the right of type 7 objects

Moving an object of type 7 to the right requires at least two spaces to the right, as follows:

            if posX < (maxCol-2):
                table()
                posX += 1
                draw()

Rotating an object of type 1

To rotate an object of type 1, to be able to rotate from 1 to 0, there must be at least 4 spaces to the right, as verified by the following code.

            actorRotate += 1
            if actorRotate > 1:
                if (posX > (maxCol-4)): 
                    actorRotate = 1
                else:
                    actorRotate = 0

Rotating an object of type 2

For object type 2, the rotation check principle is as follows.

  • To change from type 0 to type 1 rotation, there must be 2 spaces on the right.
  • To Change from type 1 to type 2 rotation, there must be 3 units of space to the right.
  • To change from type 2 to type 3 rotation, there must be at least 2 spaces on the right side.
  • To change from type 3 to type 0 rotation, at least 3 spaces to the right must be left.
  • Other cases cannot be rotated.

Code

            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3

Rotating an object of type 3

The principle is the same as for object type 2 as follows:

            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3

Rotating an object of type 4

For object type 4 rotation, the principle is the same as object type 2 and type 3 rotation as the following rotation check code.

            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3

Rotating an object of type 5

Rotating an object of type 5 has the following conditions:

  • Turn from state 0 to 1, there must be 2 spaces to the right.
  • To change from state 1 to 0, there must be at least 3 spaces to the right.
  • Other cases cannot be rotated.

Code

            if actorRotate == 0:
                actorRotate = 1
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 1

Rotating an object of type 6

Rotating an object of type 6 has the same principle as rotating an object of type 5 as follows:

            if actorRotate == 0:
                actorRotate = 1
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 1

Rotating an object of type 7

Since an object of type 7 is a 2×2 square, keeping the rotation values the same, this type of object’s rotation check is skipped.

Example Code

From the data structure and definitions about receiving values and conditions for receiving/displaying values. Write the code as follows:

  • swM1 for exiting the program
  • swM2 for random new objects
  • swA for rotating objects
  • keL to move the object position to the left
  • keR to move the object position to the right
#################################################################
# tetris
# JarutEx 2021-11-06
#################################################################
import gc
import os
import sys
import time
import machine as mc
from machine import Pin,SPI
import math
import st7735 as tft
import vga1_16x16 as font
import random
#################################################################
###### setting ##################################################
#################################################################
gc.enable()
gc.collect()

mc.freq(240000000)

spi = SPI(2, baudrate=26000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
#### Key
keL = Pin(39, Pin.IN, Pin.PULL_UP)
keU = Pin(34, Pin.IN, Pin.PULL_UP)
keD = Pin(35, Pin.IN, Pin.PULL_UP)
keR = Pin(32, Pin.IN, Pin.PULL_UP)

swM1 = Pin(33, Pin.IN, Pin.PULL_UP)
swM2 = Pin(25, Pin.IN, Pin.PULL_UP)
swA = Pin(26, Pin.IN, Pin.PULL_UP)
swB = Pin(27, Pin.IN, Pin.PULL_UP)

spk = Pin(19,Pin.OUT)
spk.on()
time.sleep(0.1)
spk.off()

# dc, rst, cs
scr = tft.ST7735(spi, 128, 160, dc=Pin(15, Pin.OUT), reset=Pin(13,Pin.OUT), cs=Pin(2, Pin.OUT), rotation=3)
scr.initr()

actors = [
    [ # แบบ1
     [[1,1,1,1], # rotate=0
      [0,0,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,0,0,0],
      [1,0,0,0],
      [1,0,0,0]]
    ],
    [ # แบบ 2
     [[1,1,1,0], # rotate=0
      [1,0,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=1
      [0,1,0,0],
      [0,1,0,0],
      [0,0,0,0]],
     [[0,0,1,0], # rotate=2
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=3
      [1,0,0,0],
      [1,0,0,0],
      [0,0,0,0]]
    ],
    [ # แบบ 3
     [[1,0,0,0], # rotate=0
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,1,0,0], # rotate=1
      [1,0,0,0],
      [1,0,0,0],
      [0,0,0,0]],
     [[1,1,1,0], # rotate=2
      [0,0,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=3
      [0,1,0,0],
      [1,1,0,0],
      [0,0,0,0]]
    ],
    [ # แบบ 4
     [[0,1,0,0], # rotate=0
      [1,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,1,0,0],
      [1,0,0,0],
      [0,0,0,0]],
     [[1,1,1,0], # rotate=2
      [0,1,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=3
      [1,1,0,0],
      [0,1,0,0],
      [0,0,0,0]],
    ],
    [ # แบบ 5
     [[0,1,1,0], # rotate=0
      [1,1,0,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[1,0,0,0], # rotate=1
      [1,1,0,0],
      [0,1,0,0],
      [0,0,0,0]]
    ],
    [ # แบบ 6
     [[1,1,0,0], # rotate=0
      [0,1,1,0],
      [0,0,0,0],
      [0,0,0,0]],
     [[0,1,0,0], # rotate=1
      [1,1,0,0],
      [1,0,0,0],
      [0,0,0,0]]
    ],
    [ # แบบ 7
     [[1,1,0,0], # rotate=0
      [1,1,0,0],
      [0,0,0,0],
      [0,0,0,0]]
    ]
]
actorColors = [
    tft.color565(232,232,64),
    tft.color565(232,64,64),
    tft.color565(232,64,232),
    tft.color565(64,64,232),
    tft.color565(64,232,64),
    tft.color565(64,232,232),
    tft.color565(232,232,232)
]
actorRotate = 0
actorNo = random.randint(0,len(actors)-1)
maxCol = 10
maxRow = 16
posX = 0
posY = 0

#################################################################
###### sub modules ##############################################
#################################################################
def splash():
    scr.fill(tft.color565(0x00,0x00,0x00))
    scr.text(font,"JarutEx", 20, 20, tft.YELLOW, tft.BLACK)
    scr.text(font,"JarutEx", 21, 20, tft.YELLOW, tft.BLACK)
    scr.text(font,"(C)2021", 40,48, tft.CYAN, tft.BLACK)
    time.sleep_ms(2000)
    scr.fill(tft.BLACK)

def table():
    blankColor = tft.color565(48, 48, 48)
    for i in range(maxRow):
        for j in range(maxCol):
            x = j * 8 + 1
            y = i * 8 + 1
            w = 6
            h = 6
            scr.fill_rect(x,y,w,h, blankColor)

def draw():
    actor = actors[actorNo][actorRotate]
    for i in range(4):
        for j in range(4):
            if actor[i][j]:
                x = (posX+j) * 8 + 1
                y = (posY+i) * 8 + 1
                w = 6
                h = 6
                scr.fill_rect(x,y,w,h,actorColors[actorNo])


#################################################################
###### main program #############################################
#################################################################
splash()
table()
draw()
while swM1.value():
    if swM2.value() == 0:
        table()
        actorNo = random.randint(0,len(actors)-1)
        posX = 0
        posY = 0
        actorRotate = 0
        draw()
    if swA.value() == 0: # rotate
        table()
        if (actorNo == 0):
            actorRotate += 1
            if actorRotate > 1:
                if (posX > (maxCol-4)): 
                    actorRotate = 1
                else:
                    actorRotate = 0
        elif (actorNo == 1):
            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3
        elif (actorNo == 2):
            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3
        elif (actorNo == 3):
            if actorRotate == 0:
                actorRotate = 1
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 3
        elif (actorNo == 4):
            if actorRotate == 0:
                actorRotate = 1
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 1
        elif (actorNo == 5):
            if actorRotate == 0:
                actorRotate = 1
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                else:
                    actorRotate = 1
        draw()
    if keL.value() == 0:
        if posX > 0:
            table()
            posX -= 1
            draw()
    if keR.value() == 0:
        if (actorNo == 0):
            if (actorRotate == 0):
                maxX = maxCol-4
            else:
                maxX = maxCol-1
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 1):
            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 2):
            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 3):
            if (actorRotate == 0):
                maxX = maxCol-3
            elif (actorRotate == 1):
                maxX = maxCol-2
            elif (actorRotate == 2):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 4):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 5):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                table()
                posX += 1
                draw()
        elif (actorNo == 6):
            if posX < (maxCol-2):
                table()
                posX += 1
                draw()
    time.sleep_ms(100)
scr.fill(0)
spi.deinit()

Conclusion

From the article, it can be seen that the design of the storage method has a fairly important effect on programming. For example, if rotation is not stored in a variable, the programmer must write a program to calculate the position after rotating the object by 90, 180 and 270 degrees, and then display it. When programming with a computer would not affect the performance but when used with a slower microcontroller, the computation takes longer and the display is delayed. However, the intention of writing code with minimal customization makes the design of data structures quite wasteful in exchange for ease of explanation and study

Finally, We hope that the example of this article was helpful and gave some ideas to those who are interested in developing simple and easy-to-make games on their own. Have fun programming.

(C) 2020-2022, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2022-01-17