Snake programmieren

Jetzt haben wir schon so viel mit der Matrix gearbeitet, dass es an der Zeit ist, diese auch mal wirklich praktisch einzusetzen. Wir werden dafür ein Snake Spiel programmieren. Das Spielprinzip von Snake ist recht simpel. Es gibt eine Schlange, die mit Pfeiltasten bewegt werden kann. Außerdem gibt es einen Apfel. Wenn die Schlange einen Apfel gegessen hat, wird diese länger und ein neuer Apfel erscheint. Das Spiel ist dann verloren, wenn die Schlange sich selbst oder die Wand berührt. Je mehr Äpfel die Schlange gegessen hat, desto länger wird sie und auch die Bewegungen werden schneller. Denn mit den Pfeiltasten kann nur die Richtung festgelegt werden, aber nicht wann sich die Schlange bewegt. Heißt: es wird immer schwieriger. Die Matrix eignet sich perfekt, um so ein Spiel darzustellen. Der Apfel ist eine einzelne angeschaltete LED, eine Schlange besteht natürlich auch aus LEDs und wird so auch immer länger. Wenn der Spieler gestorben ist, wird das angezeigt, ebenso wie die Punktezahl, die der Spieler erreicht hat. Um ein neues Spiel zu starten, muss dann nur eine Taste gedrückt werden.

Du bekommst zuerst den Programmcode und erst danach werden über die Umsetzung der Spiellogik und den genauen Programmcode sprechen. Wenn du willst, kannst du den Programmcode abtippen, Übung macht den Meister. Alternativ kannst du diesen aber auch im Internet mit dem folgenden Befehl herunterladen:

$ wget cw42.de/p/snake.py

snake.py

import RPi.GPIO as gpio
import time
from MyMax7219 import MyMatrix
from random import randint

gpio.setmode(gpio.BCM)
taster = [14,15,18,23]
matrixe = 2
matrix = MyMatrix(cascaded=matrixe)
height = 7
width = (8*matrixe)-1

def steuerung(gpio):
  print('steuerung')
  print(gpio)
  global richtung
  if(gpio == 14):  #rechts
    richtung = [1,0]
  elif(gpio == 15): #oben
    richtung = [0,-1]
  elif(gpio == 18): #unten
     richtung = [0,1]
  elif(gpio == 23): #links
    richtung = [-1,0]

for i in taster:
  gpio.setup(i,gpio.IN,pull_up_down=gpio.PUD_UP)
  gpio.add_event_detect(i, gpio.FALLING, 
  callback=steuerung)
def startSpiel():
  global snake, richtung, apfel
  snake = [[randint(2,width-4),randint(3,height-3)]]
  richtung = [0,0]
  while richtung == [0,0]:
    matrix.showMessage("READY")
  neuerApfel()

def neuerApfel():
  global apfel, snake
  apfelSnake = False
  while apfelSnake == False:
    apfelSnake = True
    apfel = [randint(0,width),randint(0,height)]
    for i in snake:
      if(i == apfel):
        apfelSnake = False
  print(apfel)

def endOfGame():
  for i in range(0,2):
    matrix.clear()
    for i in range(0,width+1):
      for j in range(0,height+1):
        matrix.pixel(i,j,1)
        time.sleep(0.001)
    time.sleep(0.01)
  matrix.showMessage("GAME OVER")
  punkte = len(snake)-1
  matrix.showMessage(str(punkte)+" PUNKTE")

  startSpiel()

startSpiel()

while True:
  keinePause = False
  newSnake = [snake[0][0]+richtung[0],
              snake[0][1]+richtung[1]]
  for i in snake:
    if(i == newSnake):
      endOfGame()
      pass

  if(newSnake == apfel):
     neuerApfel()
     keinePause = True
  else:
     snake.pop()
  snake.insert(0,newSnake)

  if(snake[0][0] > width or snake[0][1] > height
    or snake[0][0] < 0 or snake[0][1] < 0 ):
    endOfGame()
    pass

  matrix.clear()
  for i in snake:
    matrix.pixel(i[0],i[1], 1)
  matrix.pixel(apfel[0],apfel[1],1)
  if(keinePause == False):
    newLength = (len(snake)-2)*0.01
    time.sleep(0.3-newLength)
  else:
    time.sleep(0.3)

Wenn du das Programm ausführst und alles richtig angeschlossen hast, solltest du jetzt mit den Tasten die Schlange bewegen können. Damit du die Taster unterscheiden kannst, haben wir auch noch Aufkleber mit Pfeiltasten.

Grundsätzlich besteht das Programm erstmal aus dem Anfang mit den Funktionen und Einstellungen und dem Inhalt der while-Schleife, welche sich um die Bewegungen und die Regeln im Spiel kümmert. Es gibt folgende (globale) Variablen:

tasterEine Liste mit allen Tastern, damit wir diese schnell einrichten können
matrixeDie Anzahl der angeschlossenen Matrizen, in unserem Fall 1
heightIst ein Int, in dem gespeichert wird, wie hoch das Spielfeld ist
widthIst ein Int, in dem gespeichert wird, wie lang das Spielfeld ist und setzt sich aus der Anzahl der Matrizen zusammen
matrixIst das Matrixobjekt, mit dem wir schon vorher gearbeitet haben
snakeIst die Schlange und eine multidimensionale Liste. Hier werden die Koordinaten der Schlange gespeichert
richtungIst eine Liste mit x und y Koordinaten, in denen sich die Schlange bewegt
apfelIst eine Liste mit Koordinaten für den Apfel
keinePauseIst ein Boolean, der für die Geschwindigkeit der Schlange entscheidend ist
newSnakeIst eine Liste, mit der neuen Position der Schlange
newLengthIst ein Integer, der die Länge der Schlange zählt und somit die Geschwindigkeit beeinflusst

Jetzt, da wir die Variablen durchgegangen sind, gehen wir Schritt für Schritt durch den Programm-Code.

In Zeile 6 bis 24 kümmern wir uns um das Festlegen der Größen der Matrix und die Steuerung. Dafür werden in Zeile 7 zunächst die einzelnen GPIO-Pins in der Variable taster gespeichert. In Zeile 26 werden diese dann mit Hilfe einer for-Schleife alle als Inputs gesetzt und mit der Funktion steuerung() verbunden.

Diese Funktion wird in Zeile 13 bis 24 definiert. Da es sich bei der Variablen richtung um eine globale Variable handelt (wir wollen also diese Variablen auch außerhalb der Funktion verändern), müssen wir das erst in noch in Zeile 16 angeben. Wenn wir diese Zeile einfach weglassen würden, würde zwar kein Fehler auftreten, aber wir würden nicht die Variable außerhalb der Funktion verändern. Diese müssen wir dann aber auch außerhalb der Funktion ändern, damit diese überhaupt einen Einfluss hat. Als Parameter wird der GPIO-Pin übergeben, deswegen haben wir mit den if-Bedingungen in Zeile 17 bis 24 kontrolliert, welcher Taster gedrückt wurde. Daraufhin verändert sich die Richtung der xy-Koordinaten. Durch die Kommentare ist zu sehen, was welche Richtung ist.

In Zeile 30 ist die zweite Funktion startSpiel(). Mit dieser werden alle Variablen gesetzt, um das Spiel zu starten. Dafür müssen wir natürlich erstmal in Zeile 32 die Schlange erstellen. Damit diese nicht immer auf der gleichen Position anfängt, benutzen wir die randint() Funktion und ziehen noch einen kleinen Sicherheitsabstand ab. Wir wollen ja auch nicht, dass der Spieler direkt am Rand anfängt und dann eventuell sofort stirbt, weil dieser in die Wand fährt. Damit das Spiel nicht sofort anfängt – sondern erst wenn ein Taster gedrückt wurde, gibt es folgende Konstruktion in Zeile 34 bis 35, die nichts Anderes sagt als: zeige „READY“ an, bis die Richtung geändert wird – also eine Taste gedrückt wurde. In Zeile 36 wird dann die Funktion neuerApfel()aufgerufen und damit ein neuer Apfel erstellt.

Kommen wir also zu dieser Funktion. Natürlich wollen wir, dass der Apfel zufällig erstellt wird, aber wir wollen nicht, dass der Apfel dort auftaucht, wo sich der Körper der Schlange befindet. In Zeile 41 gibt es eine while-Schleife, die solange läuft wie die Variable apfelSnake False ist. In der Schleife wird in Zeile 42 diese Variable erstmal auf True gesetzt. Danach wird die Variable apfel mit zufälligen Koordinaten gefüllt. In Zeile 44 wird dann die ganze Schlange mit einer for-Schleife durchgegangen. In Zeile 45 wird dann geprüft, ob der momentane Teil der Schlange die gleichen Koordinaten hat wie der vorher erstellte apfel. Wenn das der Fall ist, wird die Variable apfelSnake wieder auf False gesetzt und folglich fängt das ganze Prozedere der while-Schleife wieder von vorne an – solange bis ein Apfel gefunden wird, der nicht auf den Koordinaten der Schlange liegt.

Die nächste Funktion heißt endOfGame() und kümmert sich um das Ende des Spiels. Wir wollen eine kleine Animation anzeigen und natürlich auch sagen, wie viele Punkte der Spieler geschafft hat. Diese Animation erzeugen wir in Zeile 50 bis 56, indem wir zweimal die gesamte Matrix zum Leuchten bringen. Anschließend zeigen wir die „GAME OVER“-Nachricht an. In Zeile 58 rechnen wir dann die erreichte Punktzahl aus. Dafür müssen wir einfach nur die Länge der Variablen snake zählen und minus eins rechnen, nämlich die Schlange, die es schon am Anfang gab. In Zeile 61 wird dann die Funktion startSpiel() aufgerufen. Denn alles, was ein Ende hat, muss ja auch wieder einen Anfang haben.

Jetzt, da wir die ganzen Funktionen definiert haben, müssen wir das Spiel noch starten. Das machen wir in Zeile 63 indem die Funktion startSpiel() zum ersten Mal aufgerufen wird.

In Zeile 65 startet jetzt die while-Schleife für die ganzen Bewegungen im Spiel.

Zuerst setzen wir die Variable keinePause auf False. Diese entscheidet nachher wie lange die Pause ist. In Zeile 67 und 68 wird mit den neuen Koordinaten vom Kopf der Schlange eine neue Liste erstellt. Dafür wird die erste Position der Schlange (also der Kopf) mit der Richtung der momentanen Bewegung zusammen gerechnet. So kommen wir einfach auf die neue Position des Kopfes.

In Zeile 69 bis 72 wird eine erste Regel getestet. Wenn die Schlange in sich selber läuft, wird das Spiel beendet. Dafür wird wieder eine for-Schleife und eine if-Abfrage benutzt. Sollte das zutreffen, wird die die Funktion endOfGame() aufgerufen und mit pass fängt die while-Schleife wieder von vorne an.

Im nächsten Schritt wird von Zeile 74 bis 78 geprüft, ob die Schlange den Apfel erreicht hat. Wenn das der Fall ist, wird zuerst in Zeile 75 die Funktion neuerApfel() aufgerufen, damit ein neuer Apfel erscheint. In Zeile 76 wird dann die Variable keinePause auf True gesetzt. Dies ist entscheidend, damit der Spieler nicht das Gefühl hat, dass die Schlange stehen geblieben ist. Da immer eine neue Position hinzugefügt wird, müssen wir jetzt auch das letzte Glied der Schlange entfernen, wenn diese keinen Apfel gefressen hat, ansonsten würde diese unkontrolliert wachsen. Dafür wird in Zeile 78 die Funktion .pop() aufgerufen, welche das letzte Element einer Liste entfernt.

Bevor wir die letzte Prüfung machen, fügen wir die neue Position der Schlange in Zeile 79 hinzu. Das machen wir mit der Funktion .insert(), mit der wir Inhalte in Listen hinzufügen können. Als ersten Parameter übergeben wir die Position. Da diese an den Anfang soll, wählen wir die 0 und als zweiten Parameter, was hinzugefügt werden soll.

In Zeile 81 bis 84 passiert etwas Ähnliches, nur dass diesmal getestet wird, ob sich der neue Kopf außerhalb des Spielfeldes befindet. Wenn das der Fall ist, wird wieder die Funktion endOfGame() aufgerufen.

Im letzten Teil werden zuerst in Zeile 87 bis 89 zuerst die Schlange und dann der Apfel dargestellt.

Kommen wir zum letzten Punkt, der Geschwindigkeit. Wenn die Schlange nicht gewachsen ist, berechnet sich die Geschwindigkeit aus der Länge der Schlange. Dazu wird in der Variablen newLength die Länge der neu hinzugekommenen Glieder gemessen und mal 0,01 gerechnet. Diese werden dann in der nächsten Zeile der 0,8-sekündigen Pause abgezogen. Diese Werte kannst du natürlich auch anpassen, wenn du ein schnelleres Spiel haben willst. Die Ausnahme gibt es in den nächsten Zeilen. Wenn ein neuer Apfel dazugekommen ist, muss die Pause ein wenig kürzer sein.

Dieser Programmcode ist ein gutes Beispiel, das zeigt, welchen Vorteil Funktionen haben und wie sie den Code übersichtlicher machen können.

Mehr als eine Matrix

Bis jetzt haben wir immer nur mit einer Matrix gearbeitet, aber diese lassen sich einfach erweitern. Mit den Anschlüssen, die oben sind, lässt sich eine weitere LED Matrix anschließen. Diese verbindest du einfach genau mit den Pins in der selben Reihenfolge, wie diese auch oben angeschlossen sind.

Bevor das Snake Spiel jetzt auch beide Matrizen anzeigt, musst du erst noch in Zeile 8 folgende Variable auf die Anzahl der Matrizen ändern:

matrixe = 2 # Anzahl der Matrizen

Und wenn du jetzt das Snake-Spiel startest, kannst du auf allen angeschlossenen Matrizen spielen. Das geht, da wir in Zeile 8-11 die Höhen- und Matrizenangaben in Variablen gespeichert haben und damit diese Änderungen einfach machen.