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.
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.
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.
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.
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 aus zwei Teilen besteht, wobei diese ohne Schauben befestigt werden sollen.
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.
Das obere Teil entspricht den Außenmaßen des unteren. Es ist mit einem Ausschnitt für das Controller-Board versehen, welcher einen Offset nach innen hat, damit das Board fest sitzt.
Die Abbildung ... zeigt die Elektrische Schaltung.
Die Stromquelle stellt eine USB 5V Quelle(Netzteil oder Powerbank) dar.
Von ihr aus gehen 5V und Erdung(GND) zum Power-Board.
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 werden die nötigen Bibliotheken inkludiert. Diese sind "EEPROM.h", um auf den EEPROM-Speicher schreiben und von ihm lesen zu können, "Adafruit_NeoPixel.h", um den LED-Streifen ansteuern zu können, sowie "LiquidCrystal_I2C.h" und "Wire.h", um die Scores an das LCD übertragen zu können.
Anschließend werden über defines die entscheidenden Parameter der Hardware und des Spielverlaufs angelegt, um diese bei Bedarf einfach anpassen zu können. So können hier die Maße des Spielfelds, die Pinbelegung der I/O-Ports und die Geschwindigkeiten der Prozesse im Spiel verändert werden.
Über konstante globale Variablen werden die unterschiedlichen Tetriminos , sowie ihre jeweilige ID und Farbe festgelegt. Weitere globale Variablen werden zum Speichern von Zeiten, der Scores, den letzten Zuständen der Knöpfe, der Daten die Das aktuelle Tetrimino definieren, verschiedener Geschwindigkeiten und des Grids angelegt.
Anschließend folgen alle Funktionen, sowie abschließend das Setup und der Loop.
Im Setup werden zunächst die In- & Outputs festgelegt und der aktuelle Highscore wird aus dem EEPROM, welcher auch nach dem Neustart des Arduinio seine Daten noch enthält, ausgelesen. Als nächstes wird das Display initialisiert, die Startnachricht ausgegeben und der LED-Streifen vorbereitet.
Daraufhin werden mögliche Datenreste von vorherigen Spielen überschrieben, die erste Blocksequenz angelegt und die Timings initialisiert. Anschließend wird auf das Drücken des Rotate-Tasters gewartet, bevor das Spiel beginnt.
Nachdem die Startanimation abgespielt wurde, beginnt der loop(). Hier wird in Regelmäßigen Abständen in einer Endlosschleife auf den Spieler reagiert, der Tetrimino nach unten bewegt und das Spielfeld neu ausgegeben.
//---------------------------------------------//
// 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 = random(DIFF_PIECES) % 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[random(100) % 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;
}
//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);
//Zufallszahlen initialisieren
randomSeed(millis());
random(100);
//Tetrimino-Sequenz füllen
choose_new_piece();
//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();
}
}