Tinkering Arduino

eine kleine Welt der Elektronik.

Das Spiel 2048 Auf Dem Arduino Esplora

Die letzten Tage und Wochen habe ich viel 2048 auf dem Telefon gespielt. Das Spielprinzip ist einfach und dennoch fesselnd. Da mein Arduino Esplora noch auf das erste eigene Programm wartete, habe ich die Herausforderung angenommen und das Spiel für den Arduino nach programmiert.

Wer kein Esplora zur Verfügung hat, kann mit einem TFT Display am Arduino auch 2048 spielen. Dazu werde ich einen eigenen Beitrag mit Schaltplan veröffentlichen.

Was dabei herausgekommen ist, soll in diesem Beitrag zu lesen sein.

Erst möchte ich zu den beiden Varianten allgemein etwas schreiben. Dann werde ich ein wenig auf das Programm eingehen. Die Umsetzung mit einem Arduino werde ich dann im nächsten Beitrag beschreiben.

Der Arduino Esplora ist ein Arduino kompatibles Board mit Komponenten, die man für kleine Spiele benötigt und von älteren Spielekonsolen kennt. Zum einen wäre da ein Joystick, vier Buttons, ein Drei-Achsen Beschleunigungssensor und so einige andere Ein- und Ausgabemöglichkeiten. Weitere Informationen kann man hier nachlesen.

Neben all diesen wäre zu erwähnen, dass es auch zwei Steckleisten bietet, um ein Arduino TFT LCD Display aufzustecken. Dieses verfügt neben der Ausgabe auch noch über ein Mikro-SD-Kartenslot, den ich aber in der aktuellen Variante des Programms noch nicht verwende.

Das Arduino Esplora Board und das Arduino TFT LCD Screen habe ich mal wieder bei Watterott gekauft.

Das Programm

Das Programm Esplora_2048 ist natürlich wieder bei github veröffentlicht. Das Programm Arduino_2048 wird mit dem nächsten Beitrag auf github zum Download bereitgestellt.

Beide Repositories enthalten eine Datei Esplora_2048.ino bzw. Arduino_2048.ino, in der die Funktionen für die Steuerung (Joystick beim Esplora versus 4 Taster beim Arduino) und die Ausgabe (Landscape beim Esplora und Portrait beim Ardunio) enthalten sind. Die gemeinsamen Dateien Game_2048.cpp und Game_2048.h, sind in beiden Repositories identisch und enthalten die Logik des Spiels.

So habe ich die Logik und damit gemeinsamen Teile des Programms ausgelagert. Sollte sich an der Programmlogik etwas ändern, weil zum Beispiel die Möglichkeit geschaffen werden soll über 2048 hinaus zu spielen, reicht es, das einmal zu machen. Genau so ist es möglich, andere Displays mit dem Arduino (z.B. größere Auflösung) anzusteuern, ohne in den Funktionen für die Spielelogik danach suchen zu müssen.

Schauen wir zuerst einmal in die Programmdatei für das Esplora Board in Auszügen an.

1
2
3
4
5
#include <Esplora.h>
#include <TFT.h>
#include <SPI.h>

#include "Game_2048.h";

Die ersten drei Includes werden für das Arduino Esplora Board benötigt. In Zeile 5 wird die Game_2048.h eingebunden, und damit die Funktionen der Programmlogik.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void setup() {
  randomSeed(analogRead(0));

  EsploraTFT.begin();

  EsploraTFT.background(255, 255, 255);

  EsploraTFT.stroke(0, 0, 0);
  EsploraTFT.fill(0, 0, 0);

  drawPoints();
  drawMatrix();

  getAvailableCells();
}

Die erste Zeile in der setup()-Funktion initialisiert den Zufallsgenerator mit einem mehr oder weniger zufälligen Wert.
In Zeile 4-9 wird das TFT-Display initialisiert, die Hintergrundfarbe, die Linienfarbe und Füllfarbe gesetzt.
In den Zeilen 11 und 12 wird das Spielfeld initial gezeichnet. Der Funktionsaufruf getAvailableCells() in Zeile 14 ruft eine Funktion in der Game_2048.cpp-Datei auf.

Wie üblich wird nach der setup()-Funktion die Funktion loop() aufgerufen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void loop() {
  int xValue = Esplora.readJoystickX();
  int yValue = Esplora.readJoystickY();
  int startButton = Esplora.readButton(SWITCH_RIGHT);

  if ( startButton == LOW ) {
    pressedButtonStart();
  } else if ( yValue < -300 && gameStarted ) {
    joystickU();
  } else if ( yValue > 300 && gameStarted ) {
    joystickD();
  } else if ( xValue > 300 && gameStarted ) {
    joystickL();
  } else if ( xValue < -300 && gameStarted ) {
    joystickR();
  }

  delay(100);

  if ( gameStarted ) {
    drawPoints();
    drawMatrix();
  }

  if ( !gameOver && !furtherMovesArePossible() ) {
    delay(1000);
    stopGame();
    drawPoints();
    drawMatrix();
  }

}

In den Zeilen 2 und 3 wird die aktuelle Position des Joysticks ausgelesen. Der Wertebereich für X und Y liegt zwischen -512 und 512.
In Zeile 4 wird der Status für den rechten Taster ausgelesen. Dieser soll als Eingabe/Startknopf dienen.
In den Zeilen 6-16 werden je nach Status des Tasters und des Joysticks verschiedene Funktionen aufgerufen. Dabei ist zu beachten, dass der Status bei gedrückten Buttons LOW ist. Eine Aktion auf Grund der Joystick-Position soll nicht erst bei Vollausschlag erfolgen, sondern schon, wenn der Joystick knapp mehr als die Hälfte in eine der Richtungen X oder Y bewegt wurde.
In den Zeilen 21 und 22 soll das Display neu gezeichnet werden, sofern das Spiel gestartet wurde.

Beispielhaft für die Bewegung des Joysticks soll die Funktion joystickU() betrachtet werden. Die anderen Funktionen sind analog aufgebaut.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void joystickU() {
  delay(10);

  if ( Esplora.readJoystickY() < -300 ) {

    erasePoints();
    moveCellsU();
    drawPoints();

    while (Esplora.readJoystickY() < -300) {
      delay(50);
    }

    delay(50);
  }
}

In Zeile 4 wird nach einer kurzen Pause noch einmal der Wert des Joysticks ausgelesen, um ähnlich wie beim programmatischen entprellen der Taster, sicher zu gehen, dass der Wert -300 endgültig überschritten ist.
Die Zeile 6 löscht die Ausgabe der Punkte vom Display. Denn in Zeile 7 wird eine Funktion aus der Spielelogik aufgerufen. Diese könnte, je nach Spielzug die Punktezahl verändern. Da es keine direkte Möglichkeit gibt, etwas vom Display zu löschen, wird die alte Punktezahl mit der Hintergrundfarbe geschrieben. Alternativ könnte man auch ein weißes gefülltes Viereck über die alte Ausgabe der erreichten Punkte zeichnen.
In Zeile 7 wird die Funktion moveCellsU() aus Game_2048.cpp aufgerufen und verändert ggf. (falls möglich) die interne Repräsentation des Spielfeldes. In Zeile 8 wird die erreichte Punktezahl wieder ausgegeben. Dann wird in den Zeilen 10-12 so lange gewartet, bis der Joystick wieder in die Ausgangposition zurückkehrt. Genauer gesagt wird so lange gewartet, bis der Joystick den festgelegten Grenzwert von -300 wieder überschreitet.

Hiweis: Man sollte hier nicht prüfen, ob der Wert 0 annimmt, denn ggf. ist die Mechanik nicht so austariert und die Ruheposition des Joysticks liegt nicht bei 0. Für andere Spiele, wo es um Genauigkeit geht, würde man den Joystick irgendwie kalibrieren.

Im folgenden gibt es eigentlich nur zwei weitere Funktionen drawMatrix() und drawGameOver(). Die erste Funktion drawMatrix() zeichnet das Spielfeld, in dem es mit zwei verschachtelten Schleifen über die interne Repräsentation des Spielfeldes geht, die Werte an der jeweiligen Position ausliest und dann entspechend zum ausgelesenen Wert die Hintergrundfarbe des Feldes und ein Offset für die Position der Textausgabe setzt.

Auf die Implementierung des Programms selbst möchte ich in diesem Beitrag nicht direkt eingehen, denn das würde den Rahmen sprengen.

Anmerkungen zum Programm

Wer sich den Aufruf der Switch-Anweisung in der Funktion drawMatrix() genauer anschaut, wird sehen, dass derzeit noch auf die interne Repräsentation des Spielfeldes zurückgegriffen wird, und somit Wissen darüber notwendig ist. Weiterhin kann man daraus ableiten, dass das Spielfeld in der internen Repräsentation nicht als Feld (zweidimensionales Array), sondern als eindimensionales Array abgebildet wird. Das sollte in einer nächsten Version noch behoben werden. Auch an anderen Stellen findet man noch Programmbestandteile, die bei der Spielelogik besser aufgehoben wären. Man muss aber dabei immer ein Auge auf den benötigten Speicherplatz des Programms schauen, der mit der weiteren Entkopplung des Programms mit der Spielelogik sicherlich auch anwachsen dürfte.

Ausblick

Wie man aus der Funktion drawMatrix() auch noch entnehmen kann, gibt es derzeit nur eine Art, wie das Spiel beendet wird. Wenn kein Zug mehr möglich ist, wird das Spiel mit einem Game Over beendet. Der Programmteil in der Spielelogik für den Fall, dass einmal 2048 erreicht wird, fehlt noch. Wie schon angedeutet, könnte man in diesem Fall auch noch eine History und Bestenliste auf der SD-Karte speichern. Auch könnte man beim Esplora ohne weiteres eine akustisches oder visuelle Rückmeldung einbauen. Eine mögliche Stelle wäre, wenn eine gewünschte Bewegung nicht möglich ist. Eine Einschränkung gegenüber dem Browser-Spiel mit JavaScript, wird man mit dem Arduino an dieser Stelle nicht zufriedenstellend lösen können. Wer das Original kennt, weiß, dass das Zusammenführen von benachbarten Feldern mit einem Slide-Effekt erfolgt. Hier ist der Arduino vielleicht doch etwas zu langsam um Vergleichbares zu erzeugen. Aber dass soll an dieser Stelle eine untergeordnete Rolle spielen.

Comments