[EN] Simple Tetris Ep.2

From the previous chapter, we have drawn the background, random objects, object drawing, left and right moving and rotating. In Part 2 of the article, which is the preceding final chapter of the Tetris series, the topic is about creating a backdrop as a grid data structure. If an object falls to the bottom, it converts that object to a table of data as shown in Figure 1, and improves the way the object falls and controls/renders the new object by using a timer without checking for collisions from moving left / right, checking if the falling object overlaps the previous object, rotation and row cutting, which will be discussed in the last article or Simple Tetris Ep.3

Figure 1 The game in this article

Data Structure

In this article, a data structure for background map storage has been added, which is used to store game state where objects are stuck. By creating a variable named field as a list type data and the internal data is generated as 16 rows, 10 datasets per row, and set the default value to 0 as follows:

field = []
for i in range(maxRow):
    row = []
    for j in range(maxCol):
        row.append(0)
    field.append(row)

In addition to adjusting the structure of the data, in this part 2, the work cycle has been changed, separating the display and receiving values into a timer named Render, and using a timer named Tmr for the setting of the falling value of an object from top to bottom.

Moving objects down

To have a random object fall from top to bottom, use the method to create a timer to run every 1 second and modify the posY value of the system variable. By using the timer there is a sample code like this:

from machine import Timer
Tmr = Timer(0)
Tmr.init( period=1000, mode=Timer.PERIODIC, callback=cbFalling)
...
Tmr.deinit()

The code above calls a callback function cbFalling to run every 1000 milliseconds. The main workflow of this function is to increase the posY value and draw the object in row posY in the table. If scrolling down to the last row of the table will change the data in the table from 0 at the time of creation to 1. After changing the value in the table, it will start randomly for a new object to fall. The working principle of each part is as follows.

Adding and assigning values to the last row

Incrementing the row values is done by incrementing the posY variable by 1 by writing the following code:

posY += 1

The next important issue is with many types of objects and each type has a different height, in addition, in some types, rotating an object affects the height of the object. So you have to figure out how tall each object is and specify the last row value the object can fall on, calculating from posY and maxRow.

Finding the last row that an object can live in, or the lastRow variable for each object, is as follows:

  • Object Type 1 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 15 because it is 1 unit high.
    • Rotate type 2 let lastRow = 12 because it is 1 unit high.
  • Object Type 2 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 14 because it is 2 unit high.
    • Rotate type 2 let lastRow = 13 because it is 3 unit high.
    • Rotate type 3 let lastRow = 14 because it is 2 unit high.
    • Rotate type 4 let lastRow = 13 because it is 3 unit high.
  • Object Type 3 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 14 because it is 2 unit high.
    • Rotate type 2 let lastRow = 13 because it is 3 unit high.
    • Rotate type 3 let lastRow = 14 because it is 2 unit high.
    • Rotate type 4 let lastRow = 13 because it is 3 unit high.
  • Object Type 4 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 14 because it is 2 unit high.
    • Rotate type 2 let lastRow = 13 because it is 3 unit high.
    • Rotate type 3 let lastRow = 14 because it is 2 unit high.
    • Rotate type 4 let lastRow = 13 because it is 3 unit high.
  • Object Type 5 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 14 because it is 2 unit high.
    • Rotate type 2 let lastRow = 13 because it is 3 unit high.
  • Object Type 6 is determined by rotation values as follows:
    • Rotate type 1 let lastRow = 14 because it is 2 unit high.
    • Rotate type 2 let lastRow = 13 because it is 3 unit high.
  • Object Type is 3 unit high so the lastRow = 14

The program code for finding the lastRow value is as follows.

    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

Changing the table to a value of 1.

When the falling object reaches the last row that can fall, we will change the value of the table field where the object is displayed to the value 1. In this article, nested of previous and current objects has not been considered. The working code in this section is as follows.

        ### update field
        for i in range(4):
            for j in range(4):
                if actors[actorNo][actorRotate][i][j] == 1:
                    field[posY+i-1][posX+j] = 1
        table()

Display control

To work on display control, use a timer named Render by running the command as follows.

Render = Timer(1)
Render.init( period=100, mode=Timer.PERIODIC, callback=cbRender)
...
Render.deinit()

From the code above, the timer is set to call cbRender every 100 milliseconds, or 10 times per second. The callback function has two main functions: data import and display.

Receiving data

Data ingestion is the same as in the original article but omits swM2 intercepting for random objects and moving the rest of the whole into the cbRebder function, bringing the data rate from the switch to 10 times per second and changing the display control itself from where the code is executed:

table() # clear screen and draw table
...process
draw() # draw object

display setting via updated variable where

  • False, do not draw
  • True, draw

Drawing

When the variable updated is true, the drawing is executed in two ways:

Finding the Rows to Draw

Calculating rows to be drawn uses the principle of selecting draw rows before the object’s location to subtract the original image of the object up to the current row plus the object’s height. But if the drawing is drawn beyond the bottom of the screen edge, the maximum row is set to be the last row of the table. Therefore, the code part for finding the rows to draw is determined according to the object type and the rotation of the object according to the following program code.

    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

Drawing command

The draw command has two parts: table() and draw(). The draw() command still works the same as the previous article, but the table() command has changed by allowing the programmer to specify the starting row and the last row to be drawn to reduce the burden of drawing unnecessary parts. This makes the overall operation better, and the code for table() is as follows.

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)

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.
  • swA for rotating the object.
  • keL for moving the object to the left.
  • keR for moving the object to the right.
#################################################################
# tetris Ep2
# JarutEx 2021-11-06
#################################################################
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()

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

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,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
blankColor = tft.color565(48, 48, 48)
filledColor = tft.color565(192,192,192)
updated = 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
    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
            
    #### stop on last row
    if (posY > lastRow):
        ### update field
        for i in range(4):
            for j in range(4):
                if actors[actorNo][actorRotate][i][j] == 1:
                    field[posY+i-1][posX+j] = 1
        table()
        ### random new object
        actorNo = random.randint(0,len(actors)-1)
        posX = 0
        posY = 0
        actorRotate = 0
    updated = True

def cbRender(x):
    global posY,posX,actorRotate, actorNo, updated
    ####################################################### 
    ##### Input
    ####################################################### 
    if swA.value() == 0: # rotate
        if (actorNo == 0):
            actorRotate += 1
            if actorRotate > 1:
                if (posX > (maxCol-4)): # if the position is out of bound, return to the proper position
                    actorRotate = 1
                else:
                    actorRotate = 0
                    updated = True
            else:
                updated = True
        elif (actorNo == 1):
            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 == 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
            updated = True
    if keR.value() == 0:
        if (actorNo == 0):
            if (actorRotate == 0):
                maxX = maxCol-4
            else:
                maxX = maxCol-1
            if posX < maxX:
                posX += 1
                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
                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
                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
                updated = True
        elif (actorNo == 4):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                posX += 1
                updated = True
        elif (actorNo == 5):
            if (actorRotate == 0):
                maxX = maxCol-3
            else:
                maxX = maxCol-2
            if posX < maxX:
                posX += 1
                updated = True
        elif (actorNo == 6):
            if posX < (maxCol-2):
                posX += 1
                updated = True
    ####################################################### 
    ##### Input
    ####################################################### 
    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

#################################################################
###### 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():
    pass
Tmr.deinit()
Render.deinit()
scr.fill(0)
spi.deinit()

Conclusion

In the article, it was found that the program structure was modified to accommodate the use of background grid data to record whether objects that hit the ground were in that grid position and adjust the operation from looping and delaying which the programmer can’t control the time as close to the desired since it is not known how long each line runs, the object falling and display/reception are controlled using a timer and the collision detection part, which affects the overlapping of the new falling object with the previous object. Moving an object left/right, rotation or non-rotation of an object, and eliminating occupied rows from the table will be discussed in the next article, the final part of our simple Tetris build.

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-18