LED Tetris: Unterschied zwischen den Versionen

Aus HSHL Mechatronik
Zur Navigation springen Zur Suche springen
Zeile 174: Zeile 174:


== Programmcode ==
== Programmcode ==
<div style="width:1100px; height:700px; overflow:scroll; border: hidden">
<div style="width:1100px; height:800px; overflow:scroll; border: hidden">
<pre>
<pre>
//---------------------------------------------//
//---------------------------------------------//
Zeile 192: Zeile 192:
//---------------------------------------------//
//---------------------------------------------//
//Defines
//Defines
//Debug
//#define DEBUG
#ifdef DEBUG
#define DEBUG_BEGIN(x)      Serial.begin(x)
#define DEBUG_PRINT(x)      Serial.print (x)
#define DEBUG_PRINTDEC(x)  Serial.print (x, DEC)
#define DEBUG_PRINTLN(x)    Serial.println (x)
#else
#define DEBUG_BEGIN(x)
#define DEBUG_PRINT(x)
#define DEBUG_PRINTDEC(x)
#define DEBUG_PRINTLN(x)
#endif


//Spielfeld
//Spielfeld
Zeile 221: Zeile 205:
#define BTN_ROTATE      11
#define BTN_ROTATE      11


// Tetrominos
// Tetriminos
#define PIECE_W        4
#define PIECE_W        4
#define PIECE_H        4
#define PIECE_H        4
Zeile 236: Zeile 220:
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NR_LED, LED_DATA, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NR_LED, LED_DATA, NEO_GRB + NEO_KHZ800);
LiquidCrystal_I2C lcd(0x27, 16, 2);
LiquidCrystal_I2C lcd(0x27, 16, 2);
unsigned int top_score = 0;
unsigned int score = 0;
unsigned long timeBefore = 0;
unsigned long timeNow = 0;
byte i = 0;


const byte empty[] = {0};
const byte empty[] = {0};
Zeile 421: Zeile 398:
};
};


//Scores
unsigned int top_score = 0;
unsigned int score = 0;
//Zeiten
unsigned long timeBefore = 0;
unsigned long timeNow = 0;
//Counter
byte i = 0;


//Letzte Werte der knöpfe, um mehrfachausführung zu verhindern
//Letzte Werte der knöpfe, um mehrfachausführung zu verhindern
Zeile 455: Zeile 442:
void pixel(int x, int y, long color) {
void pixel(int x, int y, long color) {
   int a;
   int a;
  //Serial.print("Pixel: ");
   if (x % 2) {
   if (x % 2) {
     a = x * GRID_H + y;
     a = x * GRID_H + y;
Zeile 462: Zeile 448:
     a = (x + 1) * GRID_H - (y + 1);
     a = (x + 1) * GRID_H - (y + 1);
   }
   }
  //Serial.print(a);
  //Serial.print(", ");
  //Serial.println(color);
   pixels.setPixelColor(a, color);
   pixels.setPixelColor(a, color);
}
}
Zeile 470: Zeile 453:
//Spielfeld anzeigen
//Spielfeld anzeigen
void draw_grid() {
void draw_grid() {
  /*Serial.println("draw_grid");
    for (int count = 0; count < NR_LED; count++) {
    Serial.print(grid[count]);
    Serial.print(",");
    if (count % GRID_W == 0) {
      Serial.println();
    }
    }*/
   int x, y;
   int x, y;
   for (y = 0; y < GRID_H; ++y) {
   for (y = 0; y < GRID_H; ++y) {
Zeile 495: Zeile 470:


void choose_new_piece() {
void choose_new_piece() {
  DEBUG_PRINTLN("choose new piece");
   if ( sequence_count >= DIFF_PIECES ) {
   if ( sequence_count >= DIFF_PIECES ) {
     // Liste leer
     // Liste leer
Zeile 523: Zeile 497:
}
}


void erase_piece_from_grid() { //Tetromino vom Spielfeld entfernen, um wo anders wieder einzufügen
void erase_piece_from_grid() { //Tetrimino vom Spielfeld entfernen, um wo anders wieder einzufügen
  DEBUG_PRINTLN("erase piece from grid");
   int x, y;
   int x, y;
   //Ersten Pixel des Tetrominos finden
   //Ersten Pixel des Tetriminos finden
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);


Zeile 538: Zeile 511:
       int nx = piece_x + x;                //x-Koordinate des Pixels berechnen
       int nx = piece_x + x;                //x-Koordinate des Pixels berechnen
       if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
       if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
       if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetrominos =1...
       if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetriminos =1...
         grid[ny * GRID_W + nx] = 0;        //...Pixel aus dem Grid löschen
         grid[ny * GRID_W + nx] = 0;        //...Pixel aus dem Grid löschen
       }
       }
Zeile 546: Zeile 519:


void add_piece_to_grid() {
void add_piece_to_grid() {
  DEBUG_PRINTLN("add piece to grid");
   int x, y;
   int x, y;
   //Ersten Pixel des Tetrominos finden
   //Ersten Pixel des Tetriminos finden
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);


Zeile 560: Zeile 532:
       int nx = piece_x + x;                //x-Koordinate des Pixels berechnen
       int nx = piece_x + x;                //x-Koordinate des Pixels berechnen
       if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
       if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
       if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetrominos =1...
       if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetriminos =1...
         grid[ny * GRID_W + nx] = piece_id; //...Farbe an die Stellen des Tetrominos schreiben
         grid[ny * GRID_W + nx] = piece_id; //...Farbe an die Stellen des Tetriminos schreiben
       }
       }
     }
     }
Zeile 568: Zeile 540:


void delete_row(int y) {
void delete_row(int y) {
  DEBUG_PRINTLN("delete row");
   //Score erhöhen
   //Score erhöhen
   score = score + 10;
   score = score + 10;
Zeile 610: Zeile 581:


void remove_full_rows() {
void remove_full_rows() {
  DEBUG_PRINTLN("remove full rows");
   int x, y, c;
   int x, y, c;


Zeile 627: Zeile 597:


void try_to_move_piece_sideways() {
void try_to_move_piece_sideways() {
  DEBUG_PRINTLN("try to move piece sideways");
   int new_px = 0;
   int new_px = 0;


Zeile 649: Zeile 618:


void try_to_rotate_piece() {
void try_to_rotate_piece() {
  DEBUG_PRINTLN("try to rotate piece");
   int want_turn = 0;
   int want_turn = 0;


Zeile 664: Zeile 632:
     int new_pr = ( piece_rotation + 1 ) % 4;    //Rotation erhöhen
     int new_pr = ( piece_rotation + 1 ) % 4;    //Rotation erhöhen
     if (piece_can_fit(piece_x, piece_y, new_pr)) {
     if (piece_can_fit(piece_x, piece_y, new_pr)) {
       piece_rotation = new_pr; //Wenn der Tetromino passt, drehen
       piece_rotation = new_pr; //Wenn der Tetrimino passt, drehen
     } else {
     } else {
       if (piece_can_fit(piece_x - 1, piece_y, new_pr)) {        //Wenn Tetromino weiter links passt, 1 nach links und drehen
       if (piece_can_fit(piece_x - 1, piece_y, new_pr)) {        //Wenn Tetrimino weiter links passt, 1 nach links und drehen
         piece_x = piece_x - 1;
         piece_x = piece_x - 1;
         piece_rotation = new_pr;
         piece_rotation = new_pr;
       } else if (piece_can_fit(piece_x + 1, piece_y, new_pr)) { //Wenn Tetromino weiter rechts passt, 1 nach rechts und drehen
       } else if (piece_can_fit(piece_x + 1, piece_y, new_pr)) { //Wenn Tetrimino weiter rechts passt, 1 nach rechts und drehen
         piece_x = piece_x + 1;
         piece_x = piece_x + 1;
         piece_rotation = new_pr;
         piece_rotation = new_pr;
Zeile 728: Zeile 696:
}
}


void all_white() {
void all_white() { //Alle LED nacheinander mit kurzer Verzögerung Weiß machen
   for (int led_number = 0; led_number < NR_LED; led_number++) {
   for (int led_number = 0; led_number < NR_LED; led_number++) {
     pixels.setPixelColor(led_number, pixels.Color(50, 50, 50));
     pixels.setPixelColor(led_number, pixels.Color(50, 50, 50));
Zeile 734: Zeile 702:
     delay(5);
     delay(5);
   }
   }
  DEBUG_PRINTLN("all_white");
}
}


void game_over() {
void game_over() {
   DEBUG_PRINTLN("game_over");
   //Score zurücksetzen
   score = 0;
   score = 0;
  int x, y;


  //Zeit speichern
   long over_time = millis();
   long over_time = millis();
   timeNow = millis();
   timeBefore = over_time;
   timeBefore = timeNow;
    
  //Bei erster LED beginnen
   int led_number = 0;
   int led_number = 0;
   while (HIGH) {
 
     timeNow = millis();
   while (HIGH) {   //Endlosschleife
     if (timeNow - timeBefore >= 250) {
     timeNow = millis();   //Zeit abfragen
      timeBefore += 250;
     if (timeNow - timeBefore >= 250) { //Alle 250ms...
       pixels.setPixelColor(led_number, piece_colors[rand() % DIFF_PIECES]);
       pixels.setPixelColor(led_number, piece_colors[rand() % DIFF_PIECES]);
       pixels.show();
       pixels.show();   //...Pixel zufüllige Farbe zuweisen und anzeigen
       led_number += 1;
       led_number += 1;   //Nächste LED
       led_number %= NR_LED;
       led_number %= NR_LED;   //Am Ende des Streifens von vorne beginnen
      timeBefore += 250;    //Referenzzeit erhöhen
     }
     }
 
     //restart?
 
     // click the button?
     if (!digitalRead(BTN_ROTATE) && timeNow - over_time >= 1000) {
     if (!digitalRead(BTN_ROTATE) && timeNow - over_time >= 1000) {
       break;
       break;
     }
     }
   }
   }
   setup();
   setup(); //Setup ausführen
   return;
   return;   //Mit Loop fortfahren
}
}


void try_to_drop_piece() {
void try_to_drop_piece() {
  //Tetrimino aus Grid entfernen
   erase_piece_from_grid();
   erase_piece_from_grid();
   if (piece_can_fit(piece_x, piece_y + 1, piece_rotation)) {
   if (piece_can_fit(piece_x, piece_y + 1, piece_rotation)) { //Tetrimino passt an neue Position
     piece_y++;            //Teil runter bewegen
     piece_y++;            //Teil runter bewegen
     add_piece_to_grid();
     add_piece_to_grid();
   }
   }
   else {                  //Wenn Teil nicht passt
   else {                  //Wenn Teil nicht passt
     add_piece_to_grid();
     add_piece_to_grid(); //Tetrimino wieder einfügen
     remove_full_rows();  //mögliche volle Reihen entfernen
     remove_full_rows();  //mögliche volle Reihen entfernen
     if (game_is_over() == 1) {
     if (game_is_over() == 1) { //Abfragen, ob Spiel verloren
       game_over();
       game_over();
     }
     }
Zeile 784: Zeile 752:
void try_to_drop_faster() {
void try_to_drop_faster() {


   if (!digitalRead(BTN_DOWN)) //wird der Button Down gedrückt, versuchen Tetromino runter zu bewegen
   if (!digitalRead(BTN_DOWN)) //wird der Button Down gedrückt, versuchen Tetrimino runter zu bewegen
   {
   {
     try_to_drop_piece();
     try_to_drop_piece();
Zeile 791: Zeile 759:


void react_to_player() {
void react_to_player() {
   DEBUG_PRINTLN("react_to_player");
   //Tetrimino aus Grid entfernen
   erase_piece_from_grid();
   erase_piece_from_grid();
  //Seitwärts abfragen
   try_to_move_piece_sideways();
   try_to_move_piece_sideways();
  //Rotation abfragen
   try_to_rotate_piece();
   try_to_rotate_piece();
  //Tetrimino wieder einfügen
   add_piece_to_grid();
   add_piece_to_grid();


  //Nach unten abfragen und durchführen
   try_to_drop_faster();
   try_to_drop_faster();
}
}


int game_is_over() {
int game_is_over() {
  DEBUG_PRINTLN("game_is_over");
   int x, y;
   int x, y;
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
   const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
Zeile 809: Zeile 783:
     for (x = 0; x < PIECE_W; ++x) {
     for (x = 0; x < PIECE_W; ++x) {
       if (piece[y * PIECE_W + x] > 0) {
       if (piece[y * PIECE_W + x] > 0) {
         if (ny < 0) return 1; // Tetromino ragt über den Screen, game over
         if (ny < 0) return 1; // Tetrimino ragt über den Screen, game over
       }
       }
     }
     }
Zeile 818: Zeile 792:


void setup() {
void setup() {
   DEBUG_BEGIN(115200);
   //In- und Outputs setzen
  DEBUG_PRINTLN("SETUP START");
   pinMode(BTN_LEFT, INPUT_PULLUP);
   pinMode(BTN_LEFT, INPUT_PULLUP);
   pinMode(BTN_RIGHT, INPUT_PULLUP);
   pinMode(BTN_RIGHT, INPUT_PULLUP);
Zeile 826: Zeile 799:
   pinMode(LED_DATA, OUTPUT);
   pinMode(LED_DATA, OUTPUT);


  //Highscore auslesen
   top_score = EEPROM.read(1);
   top_score = EEPROM.read(1);


  //LCD initialisieren
   lcd.init();
   lcd.init();
   lcd.backlight();
   lcd.backlight();


  //Startnachricht ausgeben
   lcd.setCursor(0, 0);
   lcd.setCursor(0, 0);
   lcd.print("Rotate to start");
   lcd.print("Rotate to start");


 
  //LED-Streifen initialisieren und löschen
 
 
   pixels.begin();
   pixels.begin();
   pixels.clear();
   pixels.clear();
   pixels.show();
   pixels.show();
  delay(1000);


  //Sicherstellen, dass das Grid leer ist
   for (i = 0; i < GRID_W * GRID_H; ++i) {
   for (i = 0; i < GRID_W * GRID_H; ++i) {
     grid[i] = 0;
     grid[i] = 0;
   }
   }


  //Zufallszahlen initialisieren
   randomSeed(millis());
   randomSeed(millis());


  //Tetrimino-Sequenz füllen
   choose_new_piece();
   choose_new_piece();


  //Timings festlegen
   move_delay = INI_MOVE_DELAY;
   move_delay = INI_MOVE_DELAY;
   drop_delay = INI_DROP_DELAY;
   drop_delay = INI_DROP_DELAY;
   draw_delay = INI_DRAW_DELAY;
   draw_delay = INI_DRAW_DELAY;


  //Anfangszeit festlegen
   last_draw = last_drop = last_move = millis();
   last_draw = last_drop = last_move = millis();
  //Auf Tastendruck warten
   while (digitalRead(BTN_ROTATE)) {}
   while (digitalRead(BTN_ROTATE)) {}
   delay(500);
   delay(500);
  //Scores auf Display schreiben
   lcd.clear();
   lcd.clear();
   lcd.setCursor(0, 0);
   lcd.setCursor(0, 0);
Zeile 865: Zeile 846:
   lcd.print("Highscore: ");
   lcd.print("Highscore: ");
   lcd.print(top_score);
   lcd.print(top_score);
  //Startanimation
   all_white();
   all_white();
}
}


void loop() {
void loop() {
  // put your main code here, to run repeatedly:
  DEBUG_PRINTLN("loop");


  //Aktuelle Zeit messen
   long t = millis();
   long t = millis();
  //Wenn genug Zeit seit letztem Move vergangen, auf Eingabe reagieren
   if (t - last_move > move_delay ) {
   if (t - last_move > move_delay ) {
     last_move = t;
     last_move = t;
     react_to_player();
     react_to_player();
   }
   }
  //Wenn genug Zeit seit letztem Drop vergangen, Tetrimono nach unten bewegen
   if (t - last_drop > drop_delay ) {
   if (t - last_drop > drop_delay ) {
     last_drop = t;
     last_drop = t;
     try_to_drop_piece();
     try_to_drop_piece();
   }
   }
  //Wenn genug Zeit seit letzter Ausgabe vergangen, Spielfeld ausgeben
   if (t - last_draw > draw_delay ) {
   if (t - last_draw > draw_delay ) {
     last_draw = t;
     last_draw = t;

Version vom 11. Januar 2022, 00:20 Uhr

Autoren: Yannick Schmidt & Nils Koch
Betreuer: Prof. Göbel & Prof. Schneider

→ zurück zur Übersicht: WS 20/21: Angewandte Elektrotechnik (BSE)

LED-Tetris Messehintergrund


Einleitung

Das Ziel der Gruppe von Yannick Schmidt & Nils Koch ist es das Spiel "Tetris" auf einer selbstgebauten 10x20 LED Matrix im Rahmen des GET-Fachpraktikums zu realisieren. Der aktuelle Score und der Highscore sollen auf einem kleinen Display angezeigt werden. Die Eingabe erfolgt per selbstgebauten Gamepad.

Anforderungen

Anforderungen
ID Inhalt Ersteller Datum Geprüft von Datum
1 Möglichst dicht gepackte LED Matrix Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
2 Rechteckiger LED Look Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
3 Start- und Endanimation Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
4 Speicherbarer Highscore Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
5 Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
6 Stromversorgung mittels USB Powerbank Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021
7 Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt Yannick Schmidt 5.10.2021 Yannick Schmidt, Nils Koch 26.10.2021



Funktionaler Systementwurf/Technischer Systementwurf

Komponentenspezifikation

Komponente Beschreibung Abbildung
Arduino UNO Microcontroller
14 digitale I/O Pins
6 analoge Eingänge
Arduino Uno Board
WS2812B ECO 3 LEDs
Rot, Grün, Blau
256 Helligkeitsstufen
WS2812B
LCD-Display I2C-Verbindung
16x2 Pixel Auflösung
Hintergrundbeleuchtung
I2C LCD Display
Gitter 3 Teile
Rechteckiger Ausschnitt für LEDs
Bohrlöcher und Stifte zum Verbauen
Gitter
Controller Design orientiert am SNES Controller
Controller-Board mittig befestigt
Clip-Verbindung zum einfachen öffnen
Controller

Umsetzung (HW/SW)

Hardware

Die Hardware besteht aus der LED-Einheit und dem Controller

LED-Einheit

Die LED-Einheit umfasst eine Bodenplatte, auf der 10 LED-Streifen des Typs WS2812b mit einer Länge von 20 LEDs in Form einer Matrix parallel aufgeklebt sind. Auf der LED-Matrix wird ein Gitter montiert, welches als Abstandshalter zur Kunststoffscheibe fungiert und einen rechteckigen Leuchteffekt der LEDs erzeugt. Unterhalb der Matrix ist das LCD-Display montiert.

Bodenplatte
Abb...Bodenplatte

Die in der Abbildung ... zu sehende Zeichnung zeigt die Bodenplatte.
Sie besteht aus einer 3mm Holzplatte, die auf 196mm Breite und 500mm Höhe zugeschnitten.
Es wurden die 6 Bohrlöcher des Gitters übertragen und der Ausschnitt des LCD-Displays ausgeschnitten.



LED-Matrix
Abb...LED-Matrix

Der in der Abbildung ... zu sehende Ausschnitt einer Zeichnung zeigt die LED-Matrix.
Sie besteht aus einem WS2812B Eco LED-Stripe, der auf 10 Abschnitte mit je 20 LEDs gekürzt wurde.
Hierbei war beim kleben zu beachten, dass die Streifen genau parallel mit 16,67mm Abstand liegen.


Gitter
Abb...LED-Matrix

Das auf Abbildung ... zu sehende Gitter ist als 3D Druck entworfen.
Jedes Loch sitzt über einer LED, wodurch sich ebenfalls ein Abstand von 16,67mm ergibt.
Das Gitter musst auf die Bodenplatte passen, wodurch sich eine Breite von 196mm ergibt.
Die Länge von 343,4mm kommt durch die Summe von 20 mal 16,67mm Abstand und 2 mal 5mm Wanddicke .
Zur Streuung des Lichts beträgt die Tiefe 16,67mm.

Durch das Bauvolumen, des Druckers Dremel 3D20, von 230x150mm musste das Gitter in 3 Teile geteilt werden.
Zur Verbindung der Gitterteile sind 4mm Stiftlöcher in die oberen und unteren Teile eingefügt worden.
Dem mittleren Teil wurden 3mm Stifte hinzugefügt.

Zur Befestigung der Gitterteile auf der Bodenplatte gibt es mittig auf jeder Seite 4mm Bohrungen.

Controller

Abb...Controller

Der in Abbildung ... zu sehende Controller ist optische dem SNES-Controller nachempfunden.

Anforderungen an den Controller waren, dass er zum einen das Controller-Board umfasst und ohne Schauben gefügt wird.
Das Controller-Board hat die Maße 70x30mm.
Es befinden sich mit dem Abstand von der Mitte aus 32,5mm und 17,5mm Löcher, um das Board zu befestigen.

Anhand dieser Maße gehen Stifte empor, die das Board in Position halten. Im unteren Teil


Elektronik

Software

Tetris ist wohl eines der am häufigsten programmierten Spiele der Welt und wurde deshalb nicht von Grund auf neu programmiert. Die Struktur des Programms wurde weitestgehend von ELECTRONOOBS übernommen. Der Code wurde optimiert und an unsere Hardware angepasst.

Zunächst wird

Programmcode

//---------------------------------------------//
//         Tetris auf dem Arduino              //
//           GET-Fachpraktikum                 //
//       Nils Koch & Yannick Schmidt           //
//          Stand: 09.01.2022                  //
//---------------------------------------------//
// Codestruktur: http://electronoobs.com/eng_arduino_tut104_code1.php
//---------------------------------------------//

#include <EEPROM.h>
#include <Adafruit_NeoPixel.h>
#include <LiquidCrystal_I2C.h>
#include <Wire.h>

//---------------------------------------------//
//Defines

//Spielfeld
#define GRID_W          10
#define GRID_H          20
#define NR_LED          GRID_W*GRID_H

//In- & Outputs
#define LED_DATA        7
#define BTN_LEFT        8
#define BTN_DOWN        10
#define BTN_RIGHT       9
#define BTN_ROTATE      11

// Tetriminos
#define PIECE_W         4
#define PIECE_H         4
#define PIECE_SIZE      PIECE_W*PIECE_H
#define DIFF_PIECES     7

//Spielgeschwindigkeit
#define DROP_MIN        70
#define DROP_ACC        20
#define INI_MOVE_DELAY  50
#define INI_DROP_DELAY  500
#define INI_DRAW_DELAY  30

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NR_LED, LED_DATA, NEO_GRB + NEO_KHZ800);
LiquidCrystal_I2C lcd(0x27, 16, 2);

const byte empty[] = {0};

const byte piece_I[] = {
  0, 0, 0, 0,
  1, 1, 1, 1,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,

  0, 0, 0, 0,
  1, 1, 1, 1,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,
};

const byte piece_T[] = {
  1, 1, 1, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  1, 1, 0, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  1, 1, 1, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 1, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,
};

const byte piece_L[] = {
  0, 0, 0, 0,
  1, 1, 1, 0,
  1, 0, 0, 0,
  0, 0, 0, 0,

  1, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,

  0, 0, 1, 0,
  1, 1, 1, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 1, 1, 0,
  0, 0, 0, 0,
};

const byte piece_J[] = {
  1, 0, 0, 0,
  1, 1, 1, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 1, 0,
  0, 1, 0, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,

  0, 0, 0, 0,
  1, 1, 1, 0,
  0, 0, 1, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 0, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
};

const byte piece_S[] = {
  0, 1, 1, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 1, 0,
  0, 0, 1, 0,
  0, 0, 0, 0,

  0, 1, 1, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 1, 0, 0,
  0, 1, 1, 0,
  0, 0, 1, 0,
  0, 0, 0, 0,
};

const byte piece_Z[] = {
  1, 1, 0, 0,
  0, 1, 1, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 0, 1, 0,
  0, 1, 1, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,

  1, 1, 0, 0,
  0, 1, 1, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  0, 0, 1, 0,
  0, 1, 1, 0,
  0, 1, 0, 0,
  0, 0, 0, 0,
};

const byte piece_O[] = {
  1, 1, 0, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  1, 1, 0, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  1, 1, 0, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,

  1, 1, 0, 0,
  1, 1, 0, 0,
  0, 0, 0, 0,
  0, 0, 0, 0,
};

const byte *pieces[DIFF_PIECES + 1] = {
  empty,
  piece_S,
  piece_Z,
  piece_L,
  piece_J,
  piece_O,
  piece_T,
  piece_I,
};

const long piece_colors[DIFF_PIECES] = {
  0x005500, // S: grün
  0x550000, // Z: rot
  0x551500, // L: orange
  0x000055, // J: blau
  0x555500, // O: gelb
  0x200020, // T: lila
  0x005555, // I: cyan
};

//Scores
unsigned int top_score = 0;
unsigned int score = 0;

//Zeiten
unsigned long timeBefore = 0;
unsigned long timeNow = 0;

//Counter
byte i = 0;

//Letzte Werte der knöpfe, um mehrfachausführung zu verhindern
byte old_button = 0;
int old_px = 0;
int old_want_turn = 0;

//Daten für aktuelles Teil
int piece_id;
int piece_rotation;
int piece_x;
int piece_y;

//Move-Geschwindigkeit
long last_move;
int move_delay;

//Drop-Geschwindigkeit
long last_drop;
int drop_delay;

//Draw-Geschwindigkeit
long last_draw;
int draw_delay;

//Spielfeld beinhaltet Farbcodes für jeden Pixel
byte grid[GRID_W * GRID_H];

//Sequenz von Teilen, um Dopplungen oder Nichtauftreten eines Steins zu vermeiden
byte piece_sequence[DIFF_PIECES];
byte sequence_count = DIFF_PIECES;

//Pixel an stelle x,y in color einfärben
void pixel(int x, int y, long color) {
  int a;
  if (x % 2) {
    a = x * GRID_H + y;
  }
  else {
    a = (x + 1) * GRID_H - (y + 1);
  }
  pixels.setPixelColor(a, color);
}

//Spielfeld anzeigen
void draw_grid() {
  int x, y;
  for (y = 0; y < GRID_H; ++y) {

    for (x = 0; x < GRID_W; ++x) {
      if (grid[y * GRID_W + x] != 0) {
        pixel(x, y, piece_colors[grid[y * GRID_W + x] - 1]);
      }
      else {
        pixel(x, y, 0);
      }

    }
  }
  pixels.show();
}

void choose_new_piece() {
  if ( sequence_count >= DIFF_PIECES ) {
    // Liste leer
    int i, j, k;
    for (i = 0; i < DIFF_PIECES; i++) {
      do {
        // Zufälliges Teil wählen
        j = rand() % DIFF_PIECES;
        // Prüfen ob schon in der Sequenz
        for (k = 0; k < i; k++) {
          if (piece_sequence[k] == j) break;
        }
        //Wenn Teil in Sequenz, neues generieren
      } while (k < i);
      // Teil hinzufügen
      piece_sequence[i] = j;
    }
    // Counter zurücksetzten
    sequence_count = 0;
  }
  // nächstes Teil laden
  piece_id = piece_sequence[sequence_count++] + 1;
  // oben in der Mitte starten
  piece_y = -4; // oberhalb des Screens beginnen
  piece_x = 4;
  piece_rotation = 0;
}

void erase_piece_from_grid() { //Tetrimino vom Spielfeld entfernen, um wo anders wieder einzufügen
  int x, y;
  //Ersten Pixel des Tetriminos finden
  const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);

  //Für jeden y-Wert...
  for (y = 0; y < PIECE_H; y++) {
    int ny = piece_y + y;                   //y-Koordinate des Pixels berechnen
    if (ny < 0 || ny > GRID_H) continue;    //Wenn außerhalb des Grids, dann Ignorieren

    //...mit jedem x-Wert:
    for (x = 0; x < PIECE_W; x++) {
      int nx = piece_x + x;                 //x-Koordinate des Pixels berechnen
      if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
      if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetriminos =1...
        grid[ny * GRID_W + nx] = 0;         //...Pixel aus dem Grid löschen
      }
    }
  }
}

void add_piece_to_grid() {
  int x, y;
  //Ersten Pixel des Tetriminos finden
  const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);

  //Für jeden y-Wert...
  for (y = 0; y < PIECE_H; y++) {
    int ny = piece_y + y;                   //y-Koordinate des Pixels berechnen
    if (ny < 0 || ny > GRID_H) continue;    //Wenn außerhalb des Grids, dann Ignorieren

    //...mit jedem x-Wert:
    for (x = 0; x < PIECE_W; x++) {
      int nx = piece_x + x;                 //x-Koordinate des Pixels berechnen
      if (nx < 0 || nx > GRID_W) continue;  //Wenn außerhalb des Grids, dann Ignorieren
      if (piece[y * PIECE_W + x] == 1) {    //Wenn Pixel im Modell des Tetriminos =1...
        grid[ny * GRID_W + nx] = piece_id; //...Farbe an die Stellen des Tetriminos schreiben
      }
    }
  }
}

void delete_row(int y) {
  //Score erhöhen
  score = score + 10;

  //Überprüfen ob neuer Top-Score erreicht wurde
  if (score > top_score)
  {
    EEPROM.update(1, score);
  }

  //Aktuellen Top-Score auslesen
  top_score = EEPROM.read(1);

  int x;

  //Zeile y und alles darüber nach unten verschieben
  for (; y > 0; y--) {
    for (x = 0; x < GRID_W; x++) {
      grid[y * GRID_W + x] = grid[(y - 1) * GRID_W + x]; //Wert aus der Zeile darüber kopieren
    }
  }

  for (x = 0; x < GRID_W; ++x) {
    grid[x] = 0;                  //oberste Zeile komplett Weiß
  }
  lcd.setCursor(0, 0);
  lcd.print("Score: ");
  lcd.print(score);
  lcd.setCursor(0, 1);
  lcd.print("Highscore: ");
  lcd.print(top_score);

  draw_grid();
  delay(200);

}

void fall_faster() {
  if (drop_delay > DROP_MIN) drop_delay -= DROP_ACC;    //Fallen beschleunigen
}

void remove_full_rows() {
  int x, y, c;

  for (y = 0; y < GRID_H; y++) {
    c = 0;
    for (x = 0; x < GRID_W; ++x) {
      if ( grid[y * GRID_W + x] > 0 ) c++; //jedes Feld zählen, das nicht leer ist
    }
    if (c == GRID_W) {      //Wenn jedes Feld belegt
      delete_row(y);        //Reihe entfernen
      fall_faster();        //Fallen beschleunigen
    }
  }
  delay(100);
}

void try_to_move_piece_sideways() {
  int new_px = 0;

  //Knöpfe einlesen
  if (!digitalRead(BTN_LEFT))
  {
    new_px = -1;
  }

  if (!digitalRead(BTN_RIGHT))
  {
    new_px = 1;
  }

  //nur bewegen, wenn sich der Bewegungswunsch geändert hat und das Teil an die neue Stelle passt
  if (piece_can_fit(piece_x + new_px, piece_y, piece_rotation) == 1) {
    piece_x += new_px;
  }
  old_px = new_px;  //Bewegungswunsch als Vergleichswert speichern
}

void try_to_rotate_piece() {
  int want_turn = 0;

  //Rotationsbutton einlesen
  int new_button = !digitalRead(BTN_ROTATE);

  //ist der Knopf gedrückt und war nicht gedrückt, Drehung versuchen
  if ( new_button > 0 && old_button != new_button ) {
    want_turn = 1;
  }
  old_button = new_button;    //Knopfzustand als Vergleichswert speichern

  if (want_turn == 1 && want_turn != old_want_turn) {
    int new_pr = ( piece_rotation + 1 ) % 4;    //Rotation erhöhen
    if (piece_can_fit(piece_x, piece_y, new_pr)) {
      piece_rotation = new_pr; //Wenn der Tetrimino passt, drehen
    } else {
      if (piece_can_fit(piece_x - 1, piece_y, new_pr)) {        //Wenn Tetrimino weiter links passt, 1 nach links und drehen
        piece_x = piece_x - 1;
        piece_rotation = new_pr;
      } else if (piece_can_fit(piece_x + 1, piece_y, new_pr)) { //Wenn Tetrimino weiter rechts passt, 1 nach rechts und drehen
        piece_x = piece_x + 1;
        piece_rotation = new_pr;
      }
    }
  }
  old_want_turn = want_turn;    //Vergelichswert speichern
}

int piece_can_fit(int px, int py, int pr) {

  //passt das Teil and die Position px,py mit der Rotation pr
  if ( piece_off_edge(px, py, pr) ) return 0;     //Nein, wenn außerhalb des Bildschirms
  if ( piece_hits_rubble(px, py, pr) ) return 0;  //Nein, wenn andere Teile überlagert
  return 1;                                       //sonst ja
}

int piece_off_edge(int px, int py, int pr) {
  int x, y;

  //Modell des Teils finden
  const byte *piece = pieces[piece_id] + (pr * PIECE_H * PIECE_W);


  for (y = 0; y < PIECE_H; ++y) {
    //Koordinaten berechnen
    int ny = py + y;
    for (x = 0; x < PIECE_W; ++x) {
      //Koordinaten berechnen
      int nx = px + x;
      if (piece[y * PIECE_W + x] > 0) {
        if (nx < 0) return 1;       // links außerhalb des Felds
        if (nx >= GRID_W ) return 1; // rechts außerhalb des Felds
      }
    }
  }
  return 0;  // auf dem Feld
}

int piece_hits_rubble(int px, int py, int pr) {
  int x, y;

  //Modell des Teils finden
  const byte *piece = pieces[piece_id] + (pr * PIECE_H * PIECE_W);

  for (y = 0; y < PIECE_H; ++y) {
    int ny = py + y;
    if (ny < 0) continue;     //Oberhalb des Screens, ignorieren
    for (x = 0; x < PIECE_W; ++x) {
      int nx = px + x;
      if (piece[y * PIECE_W + x] > 0) {
        if (ny >= GRID_H ) return 1; // Teil fällt unten aus dem Grid
        if (grid[ny * GRID_W + nx] != 0 ) return 1; // Teile überlagern sich
      }
    }
  }
  return 0;  //keine Kollision
}

void all_white() { //Alle LED nacheinander mit kurzer Verzögerung Weiß machen
  for (int led_number = 0; led_number < NR_LED; led_number++) {
    pixels.setPixelColor(led_number, pixels.Color(50, 50, 50));
    pixels.show();
    delay(5);
  }
}

void game_over() {
  //Score zurücksetzen
  score = 0;

  //Zeit speichern
  long over_time = millis();
  timeBefore = over_time;
  
  //Bei erster LED beginnen
  int led_number = 0;
  
  while (HIGH) {    //Endlosschleife
    timeNow = millis();   //Zeit abfragen
    if (timeNow - timeBefore >= 250) {  //Alle 250ms...
      pixels.setPixelColor(led_number, piece_colors[rand() % DIFF_PIECES]);
      pixels.show();    //...Pixel zufüllige Farbe zuweisen und anzeigen
      led_number += 1;    //Nächste LED
      led_number %= NR_LED;   //Am Ende des Streifens von vorne beginnen
      timeBefore += 250;    //Referenzzeit erhöhen
    }
    //restart?
    if (!digitalRead(BTN_ROTATE) && timeNow - over_time >= 1000) {
      break;
    }
  }
  setup(); //Setup ausführen
  return;   //Mit Loop fortfahren
}

void try_to_drop_piece() {
  //Tetrimino aus Grid entfernen
  erase_piece_from_grid();
  if (piece_can_fit(piece_x, piece_y + 1, piece_rotation)) { //Tetrimino passt an neue Position
    piece_y++;            //Teil runter bewegen
    add_piece_to_grid();
  }
  else {                  //Wenn Teil nicht passt
    add_piece_to_grid();  //Tetrimino wieder einfügen
    remove_full_rows();   //mögliche volle Reihen entfernen
    if (game_is_over() == 1) { //Abfragen, ob Spiel verloren
      game_over();
    }
    choose_new_piece();   //nächstes Teil wählen
  }
}

void try_to_drop_faster() {

  if (!digitalRead(BTN_DOWN)) //wird der Button Down gedrückt, versuchen Tetrimino runter zu bewegen
  {
    try_to_drop_piece();
  }
}

void react_to_player() {
  //Tetrimino aus Grid entfernen
  erase_piece_from_grid();

  //Seitwärts abfragen
  try_to_move_piece_sideways();

  //Rotation abfragen
  try_to_rotate_piece();

  //Tetrimino wieder einfügen
  add_piece_to_grid();

  //Nach unten abfragen und durchführen
  try_to_drop_faster();
}

int game_is_over() {
  int x, y;
  const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);

  for (y = 0; y < PIECE_H; ++y) {
    int ny = piece_y + y;
    for (x = 0; x < PIECE_W; ++x) {
      if (piece[y * PIECE_W + x] > 0) {
        if (ny < 0) return 1; // Tetrimino ragt über den Screen, game over
      }
    }
  }

  return 0;  //noch nicht vorbei
}

void setup() {
  //In- und Outputs setzen
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_ROTATE, INPUT_PULLUP);
  pinMode(LED_DATA, OUTPUT);

  //Highscore auslesen
  top_score = EEPROM.read(1);

  //LCD initialisieren
  lcd.init();
  lcd.backlight();

  //Startnachricht ausgeben
  lcd.setCursor(0, 0);
  lcd.print("Rotate to start");

  //LED-Streifen initialisieren und löschen
  pixels.begin();
  pixels.clear();
  pixels.show();

  //Sicherstellen, dass das Grid leer ist
  for (i = 0; i < GRID_W * GRID_H; ++i) {
    grid[i] = 0;
  }

  //Zufallszahlen initialisieren
  randomSeed(millis());

  //Tetrimino-Sequenz füllen
  choose_new_piece();

  //Timings festlegen
  move_delay = INI_MOVE_DELAY;
  drop_delay = INI_DROP_DELAY;
  draw_delay = INI_DRAW_DELAY;

  //Anfangszeit festlegen
  last_draw = last_drop = last_move = millis();

  //Auf Tastendruck warten
  while (digitalRead(BTN_ROTATE)) {}
  delay(500);

  //Scores auf Display schreiben
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Score: ");
  lcd.print(score);
  lcd.setCursor(0, 1);
  lcd.print("Highscore: ");
  lcd.print(top_score);

  //Startanimation
  all_white();
}

void loop() {

  //Aktuelle Zeit messen
  long t = millis();

  //Wenn genug Zeit seit letztem Move vergangen, auf Eingabe reagieren
  if (t - last_move > move_delay ) {
    last_move = t;
    react_to_player();
  }
  //Wenn genug Zeit seit letztem Drop vergangen, Tetrimono nach unten bewegen
  if (t - last_drop > drop_delay ) {
    last_drop = t;
    try_to_drop_piece();
  }
  //Wenn genug Zeit seit letzter Ausgabe vergangen, Spielfeld ausgeben
  if (t - last_draw > draw_delay ) {
    last_draw = t;
    draw_grid();
  }
}

Komponententest

Ergebnis

Zusammenfassung

Lessons Learned

Projektunterlagen

Projektplan

Projektdurchführung

YouTube Video

Weblinks

Literatur


→ zurück zur Übersicht: WS 21/22: Angewandte Elektrotechnik (BSE)