Tetris auf dem Arduino

Neben der Umsetzung von Snake auf unserem Arduino, haben wir auch eine Version von Tetris entwickelt. Auch dieses Spiel soll auf der 8×8 Matrix angezeigt werden und durch die Richtungstasten kontrolliert werden können.

Was ist Tetris überhaupt?

Tetris für den Game Boy (1989) – Quelle: Wikipedia

Das klassische Spiel Tetris wurde für den Elektronia-60-Rechner erstmals von dem russischen Programmierer Alexei Paschitnow am 6. Juni 1984 fertiggestellt. Die GameBoy Version von Tetris, die im Jahr 1989 veröffentlicht wurde, erfreute sich sehr großer Beliebtheit und macht Tetris zu einem der meistgespieltesten Spiele weltweit.

In Tetris gibt es verschiedene Bausteine, die am oberen Bildschirmrand auftauchen. Sie fallen dann langsam herunter. Während dem der Stein fällt, kann man ihn nach links und rechts schieben und um 90° drehen. Zusätzlich kann man den Stein beschleunigt fallen lassen. Ist der Stein unten angekommen, bleibt er liegen und ein neuer Stein wird erzeugt. Schafft man es, eine komplette Reihe mit Steinen zu füllen, wird diese gelöscht und die darüber liegenden Reihen fallen herunter. Das Spiel endet, sobald die Steine bis nach oben hin gestapelt sind, sodass der nächste Stein nicht erzeugt werden kann. In der originalen Version sieht man den Stein, der als nächstes erzeugt werden wird, das ist auf unserer Arduino-Version allerdings nicht möglich, da hier nur eine 8×8 Matrix zur Anzeige zur Verfügung steht.

 

Implementierung der Spiel-Logik

Das Spielfeld wird mit zwei zweidimensionalen Boolean-Arrays abgebildet. Die erste Dimension ist hierbei die Breite (x-Achse), die zweite die Höhe (y-Achse). Zu Beginn werden beide Arrays mit 0 initialisiert um das Spielfeld komplett zu leeren. Während ein Array den aktuell fallenden Stein festhält, repräsentiert das andere die anderen bereits unten aufgekommenen Steine.

Zum Spielbeginn wird ein Stein aus verschiedenen Variationen zufällig ausgewählt und am oberen Rand in der Mitte erzeugt. Während das Spiel läuft, wird ständig überprüft, ob eine Taste gedrückt wurde und entsprechender Code ausgeführt. Die Tasten links und rechts  bewegen den Stein in die entsprechende Richtung. Die obere dreht den Stein im Uhrzeigersinn um 90° und die untere sorgt dafür, dass der Stein schneller fällt. Jede Sekunde wird der fallende Stein, solange er nicht unten angekommen ist, nach unten verschoben. Ist er unten angekommen, wird der Stein in das andere Array verschoben. Es werden komplette Zeilen entfernt, sofern vorhanden, und der nächste Stein wird erzeugt. Wenn sich nach der Erzeugung des neuen Steins ein Pixel des Steins an derselben Stelle wie ein Pixel eines bereits gefallenen Steins befindet, endet das Spiel. Die resultierende Gesamtpunktzahl wird anschließend angezeigt und setzt sich aus der Anzahl der Steine zusammen, die unten angekommen sind.

Möchte man das Spiel erneut spielen, muss der Reset Button auf dem Arduino betätigt werden.

 

Tetris in Action

Und wenn der Quellcode auf den Arduino übertragen wurde, sieht das dann so aus:

Musik: Tetris Theme von Bogozi (Wikimedia)

 

Quellcode

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>

#define STOP_DROP 1
#define CONTINUE_DROP 0

#define PIN_BUTTON_RIGHT 2
#define PIN_BUTTON_LEFT 3
#define PIN_BUTTON_TOP 4
#define PIN_BUTTON_BOTTOM 5

#define DIRECTION_TOP 0
#define DIRECTION_RIGHT 1
#define DIRECTION_BOTTOM 2
#define DIRECTION_LEFT 3
#define DIRECTION_UNDEFINED -1

#define MATRIX_HORIZONTAL_LENGTH 8
#define MATRIX_VERTICAL_LENGTH 8 

Adafruit_8x8matrix matrix = Adafruit_8x8matrix(); 
bool gameState[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH] = { 0 };
bool tile[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH] = { 0 };
bool fastDrop = false;
int score = 1;
int direction = DIRECTION_UNDEFINED;

bool tileAction = false;
bool doesLoop = true;
long lastTime = 0;
long lastTime2 = 0;
boolean gameOverShown = false;
bool gameOver = false;

void setup() {
	randomSeed(analogRead(0));
	matrix.begin(0x70);
	matrix.setRotation(3);
	spawnTile();
	renderer();
}

void loop() {
	long currTime = 0;
	lastTime = millis();

	while (doesLoop) {
		currTime = millis();

		if (!gameOver)
		{
			getDirection();
			if (currTime - lastTime2 >= 200)
			{
				processInput();
				renderer();
				lastTime2 = millis();
			}

			if (currTime - lastTime >= 1000 || (currTime - lastTime >= 400 && fastDrop)) {
				tileAction = getTileAction();
				switch (tileAction) {
				case CONTINUE_DROP:
					dropTile();
					break;

				case STOP_DROP:
					stopTile();
					deleteFullRows();
					spawnTile();
					if (!isGameOver()) {
						score++;
					}
					else {
						gameOver = true;
					}
					break;

				default:
					break;
				}

				renderer();
				lastTime = millis();
			}
		}
		else
		{
			if (currTime - lastTime >= 1000)
			{
				matrix.setTextSize(1);
				matrix.setTextWrap(false); 
				matrix.setTextColor(LED_ON);

				if (!gameOverShown)
				{
					for (int8_t x = 0; x >= -56; x--) {
						matrix.clear();
						matrix.setCursor(x, 0);
						matrix.print("GAME OVER");
						matrix.writeDisplay();
						delay(100);
					}

					gameOverShown = true;
				}
				else {
					for (int8_t x = 0; x >= -56; x--) {
						matrix.clear();
						matrix.setCursor(x, 0);
						matrix.print("SCORE: ");
						matrix.print(score);
						matrix.writeDisplay();
						delay(100);
					}
				}
				lastTime = millis();
			}
		}
	}
}

void renderer() {
	matrix.clear();
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) { //aktuell fallendes Teil = @ //gefallene Teile = # //nicht belegt = - matrix.drawPixel(x, y, tile[y][x] || gameState[y][x] ? LED_ON : LED_OFF); } } matrix.writeDisplay(); } void spawnTile() { char r = random() % 8; //r = Zahl zwischen 0 und 7 char center = floor((float)(MATRIX_HORIZONTAL_LENGTH - 1) / (float)2); switch (r) { case 0: // + //+ + + tile[0][center] = true; tile[1][center - 1] = true; tile[1][center] = true; tile[1][center + 1] = true; break; case 1: //+ + + + tile[0][center - 1] = true; tile[0][center] = true; tile[0][center + 1] = true; tile[0][center + 2] = true; break; case 2: //+ + //+ + tile[0][center] = true; tile[0][center - 1] = true; tile[1][center] = true; tile[1][center - 1] = true; break; case 3: //+ //+ //+ + tile[0][center] = true; tile[1][center] = true; tile[2][center] = true; tile[2][center + 1] = true; break; case 4: // + // + //+ + tile[0][center + 1] = true; tile[1][center + 1] = true; tile[2][center] = true; tile[2][center + 1] = true; break; case 5: //+ + //+ + tile[0][center] = true; tile[0][center + 1] = true; tile[1][center] = true; tile[1][center + 1] = true; break; case 6: //+ + // + + tile[0][center - 1] = true; tile[0][center] = true; tile[1][center] = true; tile[1][center + 1] = true; break; case 7: // + + //+ + tile[0][center] = true; tile[0][center + 1] = true; tile[1][center - 1] = true; tile[1][center] = true; break; default: break; } } void deleteRow(char rowY) { for (char y = rowY; y >= 0; y--) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) { if (!(y - 1 == 0)) gameState[y][x] = gameState[y - 1][x]; else gameState[y][x] = false; } } } void deleteFullRows() { for (char y = MATRIX_VERTICAL_LENGTH - 1; y >= 0; y--) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {

			//Wenn nicht belegter Punkt in Zeile von gameState, nächste Zeile
			if (!gameState[y][x]) {
				break;
			}
			//Wenn letzter Punkt in Zeile und kein nicht belegter Punkt gefunden, Zeile löschen
			else if (x + 1 == MATRIX_HORIZONTAL_LENGTH) {
				deleteRow(y);
				y++;
			}
		}
	}
}

void stopTile() {
	//schreibt Werte von tile in gameState
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {
			if (tile[y][x]) {
				tile[y][x] = false;
				gameState[y][x] = true;
			}
		}
	}
}

void changeTileTo(bool changeTo[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH]) {
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {
			tile[y][x] = changeTo[y][x];
		}
	}
}

bool rotateTile() {
	char start[2] = { MATRIX_VERTICAL_LENGTH, MATRIX_HORIZONTAL_LENGTH }; //y,x von linker oberer Position des Teils
	char end[2] = { 0 }; // y,x von rechter unterer Position des Teils
	char center[2] = { 0 }; //y,x vom Mittelpunkt

							//ermittel Start und Ende
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) { if (tile[y][x]) { if (start[0] > y) start[0] = y;
				if (start[1] > x) start[1] = x;
				if (end[0] < y) end[0] = y;
				if (end[1] < x) end[1] = x;
			}
		}
	}
	//ermittelt Mittelpunkt anhand von Start- und Endpunkt
	center[0] = ceil(start[0] + ((float)(end[0] - start[0]) / (float)2));
	center[1] = floor(start[1] + ((float)(end[1] - start[1]) / (float)2));

	//Drehung
	bool temp[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH] = { 0 };
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {
			if (tile[y][x]) {
				//Wenn Drehung für aktuellen Punkt nicht möglich, nicht drehen
				if (center[0] + (x - center[1]) < 0 || center[1] - (y - center[0]) < 0 || (center[0] + (x - center[1])) >= MATRIX_VERTICAL_LENGTH || (center[1] - (y - center[0])) >= MATRIX_HORIZONTAL_LENGTH || gameState[center[0] + (x - center[1])][center[1] - (y - center[0])])
					return false;

				//Formeln für die Drehung:
				//y = centerY + (tileX - centerX)
				//x = centerX - (tileY - centerY)
				temp[center[0] + (x - center[1])][center[1] - (y - center[0])] = true;
			}
		}
	}

	changeTileTo(temp);
	return true;
}

bool moveRight() {
	bool temp[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH] = { 0 };

	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) { if (tile[y][x]) { if (x + 1 > MATRIX_HORIZONTAL_LENGTH - 1 || gameState[y][x + 1]) return false; //Wenn neue Position außerhalb der Matrix wäre oder Position belegt ist, nicht bewegen
				temp[y][x + 1] = true; //schreibt neue Positionen in temp.
			}
		}
	}

	changeTileTo(temp);
	return true;
}

bool moveLeft() {
	bool temp[MATRIX_VERTICAL_LENGTH][MATRIX_HORIZONTAL_LENGTH] = { 0 };

	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {

			if (tile[y][x]) {
				if (x - 1 < 0 || gameState[y][x - 1]) return false; //Wenn neue Position außerhalb der Matrix wäre oder Position belegt ist, nicht bewegen temp[y][x - 1] = true; //schreibt neue Positionen in temp. } } } changeTileTo(temp); return true; } void dropTile() { //geht Array von unten nach oben durch for (char y = MATRIX_VERTICAL_LENGTH - 1; y >= 0; y--) {
		for (char x = MATRIX_HORIZONTAL_LENGTH - 1; x >= 0; x--) {

			//Schiebt Punkte nach unten
			if (tile[y][x]) {
				tile[y][x] = false;
				tile[y + 1][x] = true;
			}
		}
	}
}

bool isGameOver() {
	//überprüft ob neu gespawntes Teil über bereits belegtem Punkt.
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) {
			if (tile[y][x]) {
				if (tile[y][x] && gameState[y][x]) {
					return true;
				}
			}
		}
	}
	return false;
}

bool getDirection() {
	fastDrop = false;

	if (buttonClicked(PIN_BUTTON_LEFT)) {
		direction = DIRECTION_LEFT;
	}
	else if (buttonClicked(PIN_BUTTON_RIGHT)) {
		direction = DIRECTION_RIGHT;
	}
	else if (buttonClicked(PIN_BUTTON_TOP)) {
		direction = DIRECTION_TOP;
	}
	else if (buttonClicked(PIN_BUTTON_BOTTOM)) {
		fastDrop = true;
		direction = DIRECTION_BOTTOM;
	}
	else
	{
		direction = DIRECTION_UNDEFINED;
	}
}

//keyEvents gibt true zurück, wenn tile verändert wurde.
bool processInput() {
	if (direction == DIRECTION_LEFT) {
		return moveLeft();
	}
	else if (direction == DIRECTION_RIGHT) {
		return moveRight();
	}
	else if (direction == DIRECTION_TOP) {
		return rotateTile();
	}

	return false;
}

bool getTileAction() {
	for (char y = 0; y < MATRIX_VERTICAL_LENGTH; y++) {
		for (char x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++) { if (tile[y][x]) { //Wenn Teil unten angekommen if (y + 1 >= MATRIX_VERTICAL_LENGTH || gameState[y + 1][x]) return STOP_DROP;
			}
		}
	}
	return CONTINUE_DROP;
}

boolean buttonClicked(int buttonPin)
{
	return digitalRead(buttonPin) == HIGH;
}

4 Gedanken zu „Tetris auf dem Arduino

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.