Tetris am AZ-Touch

This post shows how you can the well-known Tetris game with a microcontroller and a touchscreen. The AZ-Touch wall housing with 2.8 inch display or the AZ-Touch MOD wall housing with 2.4 inch display is used as the basis. An ESP32 Dev-Kit C or a D1-Mini can be used as a microcontroller. After the spring strips for display and controller have been soldered on the component side, the display can be mounted. Tip: Solder only the spring strips (also called Caterpillar strips) that are required for the respective MCU.


If the D1-Mini is to be used as a controller, you should not equip it with the simple pin bars, as the pins do not keep securein the socket. It is better to equip the D1 Mini with the combined spring/pin bars, as these pins are longer.


If you have soldered the Caterpillar bars for both MCUs, the pins of the ESP32 bar that lie under the D1 Mini should be covered with an insulating tape to avoid a conductive connection to the usb jack of the D1 mini. See picture.

Required hardware

Number

Part

Note

1

AZ-Touch Wall Housing 2.8 Inch Display


or 1

AZ-Touch MOD Wall Housing 2.4 inch Display


1

ESP32 Dev C


or 1

D1-Mini



Structure of the programme

The rules for the Tetris game are assumed to be known. Use seven different parts in this implementation. With the rotated versions, this results in 19 different parts.

Each part is stored as a constant in a matrix of 4 x 4 = 16 bytes. The seven different parts are given different colors. In the matrix, unoccupied blocks are filled with 0, occupied blocks are filled with the index on the color table.

Colors for the blocks

const uint16_t colorBlock[8] = "ILI9341_BLACK, ILI9341_YELLOW, ILI9341_RED, ILI9341_CYAN, ILI9341_GREEN, ILI9341_PURPLE, ILI9341_BLUE, ILI9341_ORANGE;

 

Bit pattern for the parts

0 = Block not set >0 index of color for the block

const uint8_t piece[20][16] =

-0, 0, 0, 0.

0, 0, 0, 0,

0, 0, 0, 0,

0, 0, 0, 0,

-0, 0, 0, 0.

0, 0, 0, 0,

1, 1, 0, 0,

1, 1, 0, 0,

{0, 2, 0, 0,




The game board itself has 16 lines with 12 columns. The indexing of the columns is from left to right, that of the rows from bottom to top.

So that the largest possible area is available for the game, no touch buttons are displayed. To start the game, touch the top quarter of the screen. The lowest quarter of the screen is used to move the parts. This strip is divided into three. Touching the left third pushes the falling Tetris part to the left, touching the middle third rotates the part and touching the right third pushes the part to the right.


In the following the sequence of the program is shown for a better understanding. A game is started by clearing the score and the playing field. Then a new part is inserted at the top. The game ends if no new part can be inserted. The flow chart shows the sequence of the main loop.



The shifts to the right and left as well as the rotation are triggered asynchronously by touch events. After checking whether the change is possible, it is carried out. If the change is not possible, everything remains unchanged.

The score is increased by 4 for each newly added part. If an entire row can be removed, the score is increased by 10. Depending on the score, the level and the speed of fall are increased.

< p> Score

Level

Fall speed

<100

1

0.9 s / line

<1000

2

0.7 s / line

<10 000

3

0.5 s / line

<100 000

4

0.3 s / line </ p >

From 100,000

5

0.1 s / line


The program

Besides the following libraries are required for the ESP32 or ESP8266 package:

  • Adafruit ILI9341
  • Adafruit GFX Library
  • XPT2046_Touchscreen
  • TouchEvent </ li>

The program automatically differentiates between ESP32 and D1-Mini and uses the corresponding pin assignments. The present version is for the 2.8 inch display. For the 2.4 inch display, the line from #define TOUCH_ROTATION = 3
must be changed to
#define TOUCH_ROTATION = 1.


 #include <Adafruit_GFX.h> //Grafik Bibliothek
#include <Adafruit_ILI9341.h> // Display Treiber
#include <XPT2046_Touchscreen.h> //Touchscreen Treiber
#include <TouchEvent.h> //Auswertung von Touchscreen Ereignissenisplay Treiber #include <XPT2046_Touchscreen.h> //Touchscreen Treiber #include <TouchEvent.h> //Auswertung von Touchscreen Ereignissen
/Aussehen
#define BACKGROUND ILI9341_GREENYELLOW //Farbe des Rahmens
#define TOPMARGIN 20 //Rand oben
#define LEFTMARGIN 12 //Rand links und rechts
#define COLUMNS 12 //Anzahl der Spalten
#define ROWS 16 //Anzahl der Zeilen
#define BLOCKSIZE 18 //Größe eines Blocks in Pixel
#define NOPIECE ILI9341_BLACK //Farb für das leere Spielfeld
#define ALLON ILI9341_DARKGREY //Farbe für alle Blöcke ein
#define BORDER ILI9341_WHITE //Farbe für den Blockrand
/Unterschiedliche Pin-Belegung für ESP32 und D1Mini
#ifdef ESP32
#define TFT_CS 5
#define TFT_DC 4
#define TFT_RST 22
#define TFT_LED 15
#define TOUCH_CS 14
#define LED_ON 0
#endif
#ifdef ESP8266
#define TFT_CS D1
#define TFT_DC D2
#define TFT_RST -1
#define TFT_LED D8
#define TOUCH_CS 0
#define LED_ON 1
#endif
#define TOUCH_ROTATION 3 //muss für 2.4 Zoll Display 1 und für 2.8 Zoll Display 3 sein
//Instanzen der Bibliotheken
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen touch(TOUCH_CS);
TouchEvent tevent(touch);

//Farben für die Blöcke
const uint16_t colorBlock[8] = {ILI9341_BLACK, ILI9341_YELLOW, ILI9341_RED, ILI9341_CYAN, ILI9341_GREEN, ILI9341_PURPLE, ILI9341_BLUE, ILI9341_ORANGE};
//Bitmuster für die Teile
//0 = Block nicht gesetzt >0 Index der Farbe für den Block
const uint8_t piece[20][16] = {
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   0, 0, 0, 0,  
   0, 0, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   1, 1, 0, 0,  
   1, 1, 0, 0},
  {0, 2, 0, 0, 
   0, 2, 0, 0,
   0, 2, 0, 0, 
   0, 2, 0, 0},
  {0, 0, 0, 0, 
   0, 0, 0, 0,
   0, 0, 0, 0, 
   2, 2, 2, 2},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   3, 3, 0, 0,  
   0, 3, 3, 0},
  {0, 0, 0, 0,  
   0, 3, 0, 0, 
   3, 3, 0, 0,  
   3, 0, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   0, 4, 4, 0,  
   4, 4, 0, 0},
  {0, 0, 0, 0,  
   4, 0, 0, 0, 
   4, 4, 0, 0,  
   0, 4, 0, 0},
  {0, 0, 0, 0,  
   5, 0, 0, 0, 
   5, 0, 0, 0,  
   5, 5, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   0, 0, 5, 0,  
   5, 5, 5, 0},
  {0, 0, 0, 0,  
   5, 5, 0, 0, 
   0, 5, 0, 0,  
   0, 5, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   5, 5, 5, 0,  
   5, 0, 0, 0},
  {0, 0, 0, 0,  
   0, 6, 0, 0, 
   0, 6, 0, 0,  
   6, 6, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   6, 6, 6, 0,  
   0, 0, 6, 0},
  {0, 0, 0, 0,  
   6, 6, 0, 0, 
   6, 0, 0, 0,  
   6, 0, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   6, 0, 0, 0,  
   6, 6, 6, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   0, 7, 0, 0,  
   7, 7, 7, 0},
  {0, 0, 0, 0,  
   0, 7, 0, 0, 
   7, 7, 0, 0,  
   0, 7, 0, 0},
  {0, 0, 0, 0,  
   0, 0, 0, 0, 
   7, 7, 7, 0,  
   0, 7, 0, 0},
  {0, 0, 0, 0,  
   0, 7, 0, 0, 
   0, 7, 7, 0,  
   0, 7, 0, 0}
};

//Speicherplatz für das Spielfeld
//0 bedeutet Block frei >0 Index der Farbe des belegten Blocks
uint8_t playGround[ROWS][COLUMNS]; 

//Globale variablen
uint8_t curPiece;  //aktuelles Tetris Teil
int8_t curCol;     //aktuelle Spalte
int8_t curRow;     //aktuelle Zeile
uint32_t score;    //aktueller Score
uint8_t level;     //aktueller Level
uint16_t interval; //aktuelles Zeitintervall für die Abwärtsbewegung

uint32_t last;     //letzter Zeitstempel


//Fuktion zeigt in der Kopfleiste den aktuellen Score und den Level an
//Abhängig vom Score wird der Level hinaufgesetzt und das Intervall verringert
void displayScore() {
  if (score < 10) {level = 1; interval = 900;}
  else if (score < 100) {level = 2; interval = 700;}
  else if (score < 1000) {level = 3; interval = 500;}
  else if (score < 10000) {level = 4; interval = 300;}
  else if (score < 100000) {level = 5; interval = 100;}
  tft.fillRect(0,0,240,20,BACKGROUND);
  tft.setTextSize(2);
  tft.setTextColor(ILI9341_BLACK);
  tft.setCursor(5,4);
  char buf[50];
  sprintf(buf,"SC: %8i LV: %i",score,level);
  tft.print(buf);
}

//Funktion um ein Tetris-Teil zu drehen. Der Parameter ist die Nummer des
//Teils das gedreht werden soll. Rückgabewert ist der Index des vgerehten
//Teils
uint8_t rotate(uint8_t pc) {
  uint8_t res = 0;
  switch (pc) {
    case 1: res = 1; break;
    case 2: res = 3; break;
    case 3: res = 2; break;
    case 4: res = 5; break;
    case 5: res = 4; break;
    case 6: res = 7; break;
    case 7: res = 6; break;
    case 8: res = 9; break;
    case 9: res = 10; break;
    case 10: res = 11; break;
    case 11: res = 8; break;
    case 12: res = 13; break;
    case 13: res = 14; break;
    case 14: res = 15; break;
    case 15: res = 12; break;
    case 16: res = 17; break;
    case 17: res = 18; break;
    case 18: res = 19; break;
    case 19: res = 16; break;
  }
  return res;
}

//Funktion testet ob eine Zeile voll belegt ist
boolean rowComplete(int8_t rpg) {
if ((rpg >= 0) && (rpg < ROWS)) {
boolean res = true;
uint8_t c = 0;
//wenn ein Block nicht belegt ist (Farbe 0),
//ist die Zeile nicht vollständig
while (res && (c < COLUMNS)) {
if (playGround[rpg][c] == 0) res = false;
c++;
}
return res;
}
} //Funkzion prüft ob es zwischen der Zeile rpc des Tetris-Teils pc und
//der Zeile rpg des Spielfelds ab der Position cpg Kolklisionen gibt.
//Wenn eine Kollision auftritt oder die letzte Zeile des Spielfelds
//erreicht wurde wird falsch zurückgegeben
boolean checkRow(uint8_t pc, int8_t rpc, int8_t cpg, int8_t rpg) {
boolean res = true;
if (rpg >= ROWS) return false;
if (rpg < 0) return true;
for (uint8_t i = 0; i<4; i++) {
if (piece[pc][rpc*4 + i]>0) {
if (((cpg+i) < 0) || ((cpg+i) >= COLUMNS)) {
res = false;
}else {
if (playGround[rpg][cpg+i] > 0) res = false;
}
}
}
return res;
} //Funktion prüft ob das Tetris Teil pc am Spielfeld an der Position
//Zeile rpg Spalte cpg (linke untere Ecke des Teils) Kollisionen auftreten
boolean checkPiece(uint8_t pc, int8_t cpg, int8_t rpg) {
boolean res = true;
uint8_t rpc = 0;
while (res && (rpc < 4)) {
res = checkRow(pc,rpc,cpg,rpc+rpg-3);
// Serial.printf("check %i = %i\n",rpc+rpg-3,res);
rpc++;
}
return res;
} //Funktion zeigt einen Block des Spielfeldes in Zeile y Spalte x mit der Farbe color an
//color ist die Farbe im 565 Format für das Display
void showBlock(uint8_t x, uint8_t y, uint16_t color) {
tft.fillRect(LEFTMARGIN+x*BLOCKSIZE+2,TOPMARGIN+y*BLOCKSIZE+2,BLOCKSIZE-4,BLOCKSIZE-4,color);
tft.drawRect(LEFTMARGIN+x*BLOCKSIZE+1,TOPMARGIN+y*BLOCKSIZE+1,BLOCKSIZE-2,BLOCKSIZE-2,BORDER);
}

//Funktion füllt einen Block des Spielfeldes in Zeile y Spalte x mit der Hintergrundfarbe
void hideBlock(uint8_t x, uint8_t y) {
tft.fillRect(LEFTMARGIN+x*BLOCKSIZE,TOPMARGIN+y*BLOCKSIZE,BLOCKSIZE,BLOCKSIZE,NOPIECE);
}

//Funktion zeigt das Tetris-Teil pc in Zeile rpg, Spalte cpg (Linke untere Ecke) an
//Die Farbe wird der Definition des Tetris-Teils entnommen
void showPiece(uint8_t pc, uint8_t cpg, uint8_t rpg) {
uint8_t color;
for (uint8_t r = 0; r<4; r++) {
for (uint8_t c = 0; c<4; c++) {
color = piece[pc][r*4+c];
if ((color > 0) && ((3-r+rpg) >= 0)) showBlock(cpg+c,rpg-3+r,colorBlock[color]);
}
}
}

//Funktion füllt die belegten Blöcke des Tetris-Teil pc in Zeile rpg,
//Spalte cpg (Linke untere Ecke) an mit Hintergrundfarbe
void hidePiece(uint8_t pc, int8_t cpg, int8_t rpg) {
uint8_t color;
for (uint8_t r = 0; r<4; r++) {
for (uint8_t c = 0; c<4; c++) {
color = piece[pc][r*4+c];
if ((color > 0) && ((3-r+rpg) >= 0)) hideBlock(cpg+c,rpg-3+r);
}
}
} //funktion füllt die Zeile row des Spielfelds mit Hintergrundfarbe und
//löscht alle Einträge für diese Zeile im Spielfeld-Speicher
void deleteRow(int8_t row) {
tft.fillRect(LEFTMARGIN,TOPMARGIN+row*BLOCKSIZE,COLUMNS * BLOCKSIZE,BLOCKSIZE,NOPIECE);
for (uint8_t i =0; i<COLUMNS; i++) playGround[row][i]=0;
}

//Funktion kopiert die Zeile srcrow in die Zeile dstrow
//Die Anzeige der Zielzeile wird vorher gelöscht. Beim
//kopieren wird die Quellzeile in der Zielzeile angezeigt
void copyRow(int8_t srcrow, int8_t dstrow) {
uint8_t col;
deleteRow(dstrow);
if ((srcrow < dstrow) && (srcrow >=0) && (dstrow < ROWS)) {
for (uint8_t c = 0; c < COLUMNS; c++) {
col = playGround[srcrow][c];
playGround[dstrow][c] = col;
if (col > 0) showBlock(c,dstrow,colorBlock[col]);
}
}
}

//Funktion zeigt alle Blöcke des Spielfeldes mit der Farbe ALLON an.
//Nach einer Pause von 500 ms wird das Sielfeld komplett gelöscht
void clearBoard() {
for (uint8_t x = 0; x<COLUMNS; x++) {
for (uint8_t y = 0; y<ROWS; y++) {
showBlock(x,y,ALLON);
}
}
delay(500);
for (uint8_t i = 0; i<ROWS; i++) {
deleteRow(i);
}
}

//Funktion überträgt das Tetris-Teil pc in den Spielfeldspeicher in der Zeile
//rpg an der Spalte cpg (linke untere Ecke)
void putPiece(uint8_t pc, int8_t cpg, int8_t rpg) {
uint8_t color;
for (uint8_t r = 0; r<4; r++) {
for (uint8_t c = 0; c<4; c++) {
color = piece[pc][r*4+c];
if ((color > 0) && ((3-r+rpg) >= 0)) playGround[rpg-3+r][cpg+c] = color;
}
}
} //Ein neues Tetristeil wird am oberen Rand des Spielfeldes eingefügt.
//Welches Teil und in welcher Spalte wird als Zufallszahl ermittelt
//Hat das neue Teil keinen Platz am Spielfeld, so ist das Spiel zu Ende
boolean newPiece() {
uint8_t pc = random(1,20);
uint8_t cpg = random(0,COLUMNS-4);
boolean res = checkPiece(pc,cpg,3);
curPiece=0;
if (res) {
curPiece = pc;
curCol = cpg;
curRow = 0;
showPiece(pc,cpg,0);
score += 4;
displayScore();
} else {
tft.setTextSize(3);
tft.setCursor(LEFTMARGIN+COLUMNS*BLOCKSIZE/2-79,TOPMARGIN+ROWS*BLOCKSIZE/2-10);
tft.setTextColor(ILI9341_BLACK);
tft.print("GAME OVER");
tft.setCursor(LEFTMARGIN+COLUMNS*BLOCKSIZE/2-81,TOPMARGIN+ROWS*BLOCKSIZE/2-12);
tft.setTextColor(ILI9341_YELLOW);
tft.print("GAME OVER");
}
} //Funktion ermittelt komplett gefüllte Zeilen am Spielfeld und entfernt diese
//Darüberliegende Zeilen werden nach unten verschoben
void removeComplete() {
uint8_t s=ROWS-1;
int8_t d= ROWS-1;
while (d >= 0) {
if (rowComplete(d)) {
s--;
score += 10;
copyRow(s,d);
} else {
if ((s < d) && (s >=0)) {
Serial.printf("copy %i to %i\n",s, d);
copyRow(s,d);
}
s--;
d--;
}
}
displayScore();
}

//Funktion beginnt ein neues Spiel. Der score wird auf 0 gesetzt, das Spielfeld
//gelöscht und mit einem neuen Tetris Teil gestartet
void newGame() {
score=0;
displayScore();
clearBoard();
newPiece();
} //Callbackfunktion für Touchscreen Ereignis Klick
//Diese Funktion wird immer dann aufgerufen, wenn der Bildschirm
//kurz berührt wird. p gibt die Position des Berührungspunktes an
void onClick(TS_Point p) {

if (p.y < 80) { //Klick im obersten Viertel des Displays
newGame();
} else if (p.y > 240) { //Klick im untersten Viertel
uint8_t pc = curPiece;
int8_t c = curCol;
if (p.x < 80) { //Klick im linken Drittel -> nach links schieben
c--;
} else if (p.x <160) { //Klick im mittleren Drittel -> drehen
pc = rotate(pc);
} else { //Klick im rechten Drittel -> nach rechts schieben
c++;
}
//nach Änderung der Position wird auf Kollision geprüft
//nur wenn keine Kollision auftritt, wird die Bewegung
//ausgeführt
if (checkPiece(pc,c,curRow)) {
hidePiece(curPiece,curCol,curRow);
curPiece = pc;
curCol = c;
showPiece(curPiece,curCol,curRow);
}
}
}

//Vorbereitung
void setup() {
Serial.begin(115200);
//Hintergrundbeleuchtung einschalten
pinMode(TFT_LED,OUTPUT);
digitalWrite(TFT_LED, LED_ON);
Serial.println("Start");
//Display initialisieren
tft.begin();
tft.fillScreen(BACKGROUND);
//Touchscreen vorbereiten
touch.begin();
touch.setRotation(TOUCH_ROTATION);
tevent.setResolution(tft.width(),tft.height());
tevent.setDrawMode(false);
//Callback Funktion registrieren
tevent.registerOnTouchClick(onClick);
//tft.fillRect(LEFTMARGIN,TOPMARGIN,COLUMNS*BLOCKSIZE,ROWS*BLOCKSIZE,NOPIECE);
clearBoard();
//newPiece();
//Startzeit merken und Spielfeld löschen
last = millis();
score=0;
displayScore();
}

//Hauptschleife
void loop() {
//Auf Touch Ereignisse prüfen
tevent.pollTouchScreen();
//Immer wenn die Zeit intterval erreicht ist, wird das aktuelle Tetris-Teil
//um eine Zeile nach unten geschoben falls das möglich ist.
//Kommt es dabei zu einer Kollision oder wird der untere Rand erreicht,
//so wird das Teil nicht verschoben sondern am Spielfeld verankert.
//Vollständige Zeilen werden entfernt und ein neues Teil am oberen
//Rand eingefügt
if ((curPiece > 0) && ((millis()-last) > interval)) {
last = millis();
if (checkPiece(curPiece,curCol,curRow+1)) {
hidePiece(curPiece,curCol,curRow);
curRow++;
showPiece(curPiece,curCol,curRow);
} else {
putPiece(curPiece,curCol,curRow);
removeComplete();
newPiece();
}
}
}


Sketch to download

Have fun playing.


DisplaysEsp-32Esp-8266Smart home

3 comments

Gerald

Gerald

Hallo TimQ,
die meisten Beispiele auch die im Smarthome-Buch arbeiten mit der AZ-Touch Version mit 2.4 Zoll Display und der alten Version der Platine, bei der ein Jumper zum Hochladen angeschlossen werden musste. Bei der neuen Platine wurde die Interruptleitung für den Touchscreen auf Pin27 geändert. Hier die notwendigen Änderungen damit die Smarthome-Zentrale aus dem Smarthome Buch funktioniert.

Das neue AZ-Touch MOD hat den Interrupt für den Touchscreen nicht mehr an Pin 2 sondern an Pin27.
Die entsprechende Definition am Anfang der Smarthome Sketches muss daher
#define TOUCH_IRQ 27
lauten.
Bei der Verwendung des 2.8 Zoll Displays muss außerdem in der Setup-Funktion nach
touch.begin();
touch.setRotation(3);
eingefügt werden.

Birger T

Birger T

Dieses Projekt sollte der Beginn meiner Beschäftigung mit den ESP32 und ESP8266 aus einem AZ-Delivery Überraschungspaket sein. Nach dem Studium der diversen E-books habe ich auch irgendwo den Hinweis gefunden, wie weitere Bordverwalter URLs in die Arduino IDE zu ergänzen sind und somit die “Boards” installiert werden können. Doch der erste Versuch mit dem Blink-Sketch funktionierte nicht: Kompilierung abgebrochen, weil das im ESP-Tool einzubindende “Serial” nicht vorhanden ist.
Ich arbeite unter Ubuntu 20.04 – und wollte hier denjenigen Ubuntu Usern, die ebenfalls Probleme mit der ESP Programmierung in der Arduino IDE das Problem mit dem “Serial” haben, einen/den Link zur Lösung mitteilen: https://koen.vervloesem.eu/blog/fixing-the-arduino-ide-for-the-esp32esp8266-on-ubuntu-2004/

TimQ

TimQ

Das ist das erste Beispiel für den AZ-Touch MOD 01-03 und ESP32, das funktioniert. Super !
Alle anderen Versuche mit den Beispielen aus den Blocks, Smarthome Buch oder den zahlreichen Foren sind bei mir gescheitert. Entweder blieb der Screen weiß oder dunkel. Ich hoffe es gibt bald mal eine wirklich funktionierende Beschreibung zur Smarthome Zentrale. Danke

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. Install ESP32 now from the board manager
  2. Lüftersteuerung Raspberry Pi
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1
  4. ESP32 - das Multitalent
  5. OTA - Over the Air - ESP programming via WLAN