[EN] Simple Tetris Ep.3

The final article on making a Simple Tetris game using MicroPython and an esp32 microcontroller, as written in parts 1 and 2 of the first two articles, is described in the article below. Readers learn to design data structures, drawing the seven types of falling objects and controlling them to move left, right, and rotate. The second article has the object fall from above and keep the object’s position state. And in this article, the falling objects can be stacked along with moving left, right, and rotating the object will check for collisions with previous objects that have fallen before. Also, check if the object falls to the bottom if there are any rows without spaces. If any rows with no spaces are found, they will be deleted. And finally added a section to check the end of the game in case there is no place for objects to fall and move again as in Figure 1, ending our simple game making process.

Figure 1 Our game in this article

Collision checking

The collision check checks if the value 1 in actors, actorNo, in the actorRotate angle, has any position or field that overlaps the background or Field of 1. If found, it returns True, as shown in the following code. This code contains conditional bindings to give readers a way to code for conditional checks using in and Python list variables is to combine the conditions of objects of type 1, 2, and 3 with objects of type 4 and 5 because they have the same width and height across all rotations. This reduces the number of if used.

if actorNo == 0:
        if actorRotate == 0:
            for j in range(4):
                if (actors[actorNo][actorRotate][0][j] & field[posY][posX+j]):
                    return True
        else:
            for i in range(4):
                if (actors[actorNo][actorRotate][i][0] & field[posY+i][posX]):
                    return True
    else:
        if actorNo in [1,2,3]:
            if actorRotate in [0,2]:
                for i in range(2):
                    for j in range(3):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
            else:
                for i in range(3):
                    for j in range(2):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
        elif actorNo in [4,5]:
            if actorRotate == 0:
                for i in range(2):
                    for j in range(3):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
            else:
                for i in range(3):
                    for j in range(2):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
        else:
            for i in range(2):
                for j in range(2):
                    if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                        return True
    return False

Moving to the left

The working principle to move to the left check is:

  • Check first that the left edge has not reached.
    • Further check that there is no collision with the left.
      • If there’s collision does not reduce posX.
      • If not, reduce posX.

Here’s an example code for responding to moving an object to the left: which will be found to use the principle of trying to move first. If you move and don’t crash, it will be considered successful. But if it moves and crashes, it will use the same value according to the steps mentioned.

    if keL.value() == 0:
        if posX > 0:
            posX -= 1
            if isCollide():
                posX += 1
            else:
                updated = True

Moving to the right

The case for shifting to the right is similar to the case for the left:

  • Check that the edge of the right side has not crash.
    • Further check that there is no collision when moving to the right.
      • If collision does not increase posX.
      • If not, increase posX.

Rotation

Rotation is different from moving to the left or right, that is, it determines when a rotation occurs is there any collision occurs, or if not the rotation is performed. But if there’s collision does not change the degree of rotation as follows.

  • If the rotation is not a problem with the right and bottom edge collisions.
    • change actorRotate
    • Collision testing
      • If crash, change actorRotate to original value.
      • If not crash, use new value.

Falling

An object’s fall relies on a collision as a determinant of the lowest position an object can land. The working principle is as follows.

  • If falling to the last row, remember the position of the object.
  • If not, check for collisions with objects below.
    • If a collision occurs, memorize its current position in the field and begin randomly dropping a new object.
    • If there is no collision, skip it.

Defeat check

Defeat checks are carried out using the condition of defeat: If the falling object doesn’t have any more drop boxes then it means game over. If you create a new object to fall and then check for collisions and find that the bottom collision means that the object can’t move anymore, end the game by setting the gameOver variable to True and stop the Tmr timer and Render and show Game Over message as shown in Figure 1 on the screen as following code.

            table()
            newItem()
            if (isCollide()): 
                gameOver = True
                Tmr.deinit()
                Render.deinit()
                return

Removing filled rows

The working principle of eliminating filled rows is as follows.

    1 row = last row
    2 If all data in row are 1.
         2.1 If it's the top row, delete the current row.
         2.2 If not, copy the top row down and change the row above to 0 for the whole row
    3 If still found row = row - 1
    4 goto 2 if row > 0

Code

    row = maxRow-1
    while row > 0:
        if ([1,1,1,1,1,1,1,1,1,1] == field[row]):
            if (row == 0): # 2.1
                field[row] = [0,0,0,0,0,0,0,0,0,0]
            else:
                for r in range(row, 1, -1):
                    field[r] = field[r-1]
                field[0] = [0,0,0,0,0,0,0,0,0,0]
        else: 
            row = row - 1

From the above code, it is found that if [1,1,1,1,1,1,1,1,1,1] which is the flag of any row in the field is found, changing the entire row is done by passing [0,0,0,0,0,0,0,0,0,0] to field[row]

Example Code

From all the principles, the Simple Tetris code is as follows:

#################################################################
# tetris Ep3
# JarutEx 2021-11-08
#################################################################
import gc
import os
import sys
import time
import machine as mc
from machine import Pin,SPI, Timer
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()

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()

Tmr = Timer(0)

field = []
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,0,0,0], # rotate=3
      [1,0,0,0],
      [1,1,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
blankColor = tft.color565(48, 48, 48)
filledColor = tft.color565(192,192,192)
updated = False
gameOver = False

#################################################################
###### 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 genTable():
    global field
    for i in range(maxRow):
        row = []
        for j in range(maxCol):
            row.append(0)
        field.append(row)

def table(rowStart=0, rowEnd=maxRow):
    for i in range(rowStart, rowEnd,1):
        for j in range(maxCol):
            x = j * 8 + 1
            y = i * 8 + 1
            w = 6
            h = 6
            if (field[i][j] == 0):
                scr.fill_rect(x,y,w,h, blankColor)
            else:
                scr.fill_rect(x,y,w,h, filledColor)

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])
                
def cbFalling(x):
    global posY,posX,actorRotate, actorNo, updated, gameOver, Tmr, Render
    posY += 1
    lastRow = 14
    if actorNo == 0:
        if actorRotate == 0:
            lastRow = 15
        else:
            lastRow = 12
    elif actorNo == 1:
        if actorRotate == 0:
            lastRow = 14
        elif actorRotate == 1:
            lastRow = 13
        elif actorRotate == 2:
            lastRow = 14
        else:
            lastRow = 13
    elif actorNo == 2:
        if actorRotate == 0:
            lastRow = 14
        elif actorRotate == 1:
            lastRow = 13
        elif actorRotate == 2:
            lastRow = 14
        else:
            lastRow = 13
    elif actorNo == 3:
        if actorRotate == 0:
            lastRow = 14
        elif actorRotate == 1:
            lastRow = 13
        elif actorRotate == 2:
            lastRow = 14
        else:
            lastRow = 13
    elif actorNo == 4:
        if actorRotate == 0:
            lastRow = 14
        else:
            lastRow = 13
    elif actorNo == 5:
        if actorRotate == 0:
            lastRow = 14
        else:
            lastRow = 13
    else:
        lastRow = 14
    
    if (posY > lastRow):
        for i in range(4):
            for j in range(4):
                if actors[actorNo][actorRotate][i][j] == 1:
                    field[posY+i-1][posX+j] = 1
        removeRow()
        table()
        newItem()
    else:
        if (isCollide()):
            for i in range(4):
                for j in range(4):
                    if actors[actorNo][actorRotate][i][j] == 1:
                        field[posY+i-1][posX+j] = 1
            removeRow()
            table()
            newItem()
            if (isCollide()): 
                gameOver = True
                Tmr.deinit()
                Render.deinit()
                return
    updated = True

def newItem():
    global actorNo, posX, posY, actorRotate
    actorNo = random.randint(0,len(actors)-1)
    posX = 0
    posY = 0
    actorRotate = 0

def isCollide():
    if actorNo == 0:
        if actorRotate == 0:
            for j in range(4):
                if (actors[actorNo][actorRotate][0][j] & field[posY][posX+j]):
                    return True
        else:
            for i in range(4):
                if (actors[actorNo][actorRotate][i][0] & field[posY+i][posX]):
                    return True
    else:
        if actorNo in [1,2,3]:
            if actorRotate in [0,2]:
                for i in range(2):
                    for j in range(3):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
            else:
                for i in range(3):
                    for j in range(2):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
        elif actorNo in [4,5]:
            if actorRotate == 0:
                for i in range(2):
                    for j in range(3):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
            else:
                for i in range(3):
                    for j in range(2):
                        if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                            return True
        else:
            for i in range(2):
                for j in range(2):
                    if (actors[actorNo][actorRotate][i][j] & field[posY+i][posX+j]):
                        return True
    return False

def cbRender(x):
    global posY,posX,actorRotate, actorNo, updated
    if swA.value() == 0: # rotate
        if (actorNo == 0):
            if (actorRotate == 0):
                if (posX > (maxCol-4))
                    pass
                else:
                    actorRotate = 1
                    if (isCollide()):
                        actorRotate = 0
                    else:
                        updated = True
            else:
                if (posY > maxRow-4):
                    pass
                else: 
                    actorRotate = 0
                    if (isCollide()):
                        actorRotate = 1
                    else:
                        updated = True
        elif (actorNo == 1):
            if actorRotate == 0:
                actorRotate = 1
                if (isCollide()):
                    actorRotate = 0
                else:
                    updated = True
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                    if (isCollide()):
                        actorRotate = 1
                    else:
                        updated = True
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
                updated = True
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                    updated = True
                else:
                    actorRotate = 3
        elif (actorNo == 2):
            if actorRotate == 0:
                actorRotate = 1
                updated = True
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                    updated = True
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
                updated = True
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                    updated = True
                else:
                    actorRotate = 3
        elif (actorNo == 3):
            if actorRotate == 0:
                actorRotate = 1
                updated = True
            elif actorRotate == 1:
                if (posX < (maxCol - 2)):
                    actorRotate = 2
                    updated = True
                else:
                    actorRotate = 1
            elif actorRotate == 2:
                actorRotate = 3
                updated = True
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                    updated = True
                else:
                    actorRotate = 3
        elif (actorNo == 4):
            if actorRotate == 0:
                actorRotate = 1
                updated = True
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                    updated = True
                else:
                    actorRotate = 1
        elif (actorNo == 5):
            if actorRotate == 0:
                actorRotate = 1
                updated = True
            else:
                if (posX < (maxCol - 2)):
                    actorRotate = 0
                    updated = True
                else:
                    actorRotate = 1
    if keL.value() == 0:
        if posX > 0:
            posX -= 1
            if isCollide():
                posX += 1
            else:
                updated = True
    if keR.value() == 0:
        if (actorNo == 0):
            if (actorRotate == 0):
                maxX = maxCol-4
            else:
                maxX = maxCol-1
            if posX < maxX:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        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:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        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:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        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:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        elif (actorNo == 4):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        elif (actorNo == 5):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
        elif (actorNo == 6):
            if posX < (maxCol-2):
                posX += 1
                if (isCollide()):
                    posX -= 1
                else:
                    updated = True
    if (updated):
        startY = 0
        endY = posY
        if posY > 0:
            startY = posY-1
        if endY >= maxRow:
            endY = maxRow
        if actorNo == 0:
            endY += 4
        elif actorNo == 1:
            endY += 3
        elif actorNo == 2:
            endY += 3
        elif actorNo == 3:
            endY += 3
        elif actorNo == 4:
            endY += 3
        elif actorNo == 5:
            endY += 3
        else:
            endY += 2
        if endY >= maxRow:
            endY = maxRow
        table(startY,endY)
        draw()
        updated = False
        
def removeRow():
    row = maxRow-1
    while row > 0:
        if ([1,1,1,1,1,1,1,1,1,1] == field[row]):
            if (row == 0): 
                field[row] = [0,0,0,0,0,0,0,0,0,0]
            else: 
                for r in range(row, 1, -1):
                    field[r] = field[r-1]
                field[0] = [0,0,0,0,0,0,0,0,0,0]
        else:
            row = row - 1


#################################################################
###### main program #############################################
#################################################################
splash()
genTable()
table()
draw()
Render = Timer(1)
Tmr.init( period=1000, mode=Timer.PERIODIC, callback=cbFalling)
Render.init( period=100, mode=Timer.PERIODIC, callback=cbRender)
while swM1.value():
    if gameOver:
        scr.text(font," Game ", 40, 40, tft.YELLOW, tft.RED)
        scr.text(font," Over ", 40, 56, tft.YELLOW, tft.RED)
        break
scr.fill(0)
spi.deinit()

Conclusion

From this article, you will find that our Tetris game can be relocated, rotate, remove filled rows correctly and the end of the game is checked. However, what is missing in the game is the sound system, pushing objects to fall without waiting, Cut-off animations, scoring, and game levels which is the part that makes the game complete, challenging and fun. 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-19