Das Spiel Senso bzw. Simon Says auf dem Raspberry Pi

In diesem Projekt baue ich auf meine vorhergehenden Projekte mit LED und passivem Lautsprecher auf und erweitere es um Taster, also ein Input-Bauteil. Auch wollen wir unsere Python Kenntnisse ausbauen. Damit wir ein praktisches Ziel vor Augen haben, soll alles zusammen ein Spiel ergeben.

Irgendwann in den früher Achtzigern - ich war nur ein kleiner Junge von 12 Jahren oder so kam ein Spiel von MB (die Fernseh-Werbung! "Goonnnngggg - MB präsentiert... SENSO!...") auf den Markt, dass in Deutschland unter dem Namen "Senso" vertrieben wurde. Im Rest der Welt nannte es sich "Simon says" oder "Touch me". Es bestand aus 4 im Kreis angeordneten, halbdurchsichtigen Feldern mit den Farben rot, blau, gelb und grün, unter denen Lämpchen verborgen waren und die man niederdrücken konnte.

Das Gerät spielte einen Ton ab und das dazu passende Lämpchen leuchtete dazu auf. Nun musste man diese Taste drücken. Dann kam ein weiterer Ton dazu und man musste beide Töne wiederholen. So ging das immer weiter : es kamen mit mehr Töne hinzu und es wurde immer schneller. Schnell geriet man an die Grenze seiner Merkfähigkeit bzw. Konzentration und man hatte das Spiel verloren, dazu reichte nur ein falscher Tastendruck.

Das Spiel stand ganz oben auf meinem Weihnachts-Wunschzettel. Das damalig 80 Mark teure, große Senso wurde es dann zwar nicht, aber der Weihnachtsmann legte die Mini Version unter den Tannenbaum. Die war eigentlich genauso gut und bereitete mir lange Freude.

Es gab einen verstellbaren Schwierigkeitsgrad von 1 bis 4, welche die nötige Tonfolgenlänge für einen Sieg markierte. Die schwierigste Stufe hatte, so weit ich mich noch erinnern kann, 22 Töne oder so, ohne aufzuschreiben für mich nicht zu schaffen. Wir brauchen für sowas in unserem Programm natürlich keinen Schiebeschalter, sondern passen einfach eine Variable. Wem 10 Stufen zum Sieg zu einfach sind, der schreibt einfach mehr rein.

Im folgenden Video ist die fertige Schaltung und der Spielablauf zu sehen. Damit kann man sich gut vorstellen, wie das Original funktioniert hat:



Die Schaltung dazu sieht so aus:



Was als erstes auffällt - außer vielleicht, dass unser Breadboard diesmal gut gefüllt ist - ist das Flachbandkabel oben rechts. Mein GPIO Cobbler ist natürlich immer noch nicht da (Versand aus China halt) und das Gestecke in das Flachbandkabel war dann doch ein bisschen eng und unübersichtlich. Darum kam ich auf die Idee, nur eine Seite der GPIO-Leiste zu verwenden und eine 20 Pin breite Pfostensteckerleiste ins Kabel zu schieben und den Rest aufs Breadboard. Wir benutzen diesmal also nur eine Seite (die Linke, 1-3-5..39). Wegen der Spiegelverkehrtheit steckt sie allerdings auf der rechten Seite - die marktierte Leitung im Flachband Kabel ist jeweils oben (Pin 1/2).



Beschaltung GPIO-Leiste auf Breadboard: (von links nach rechts) Nr. Kabel GPIO-Port 39 blau GND -> obere blaue Masse-Linie 37 gelb BCM 26 -> Vorwiderstand gelbe LED 68 Ω -> Pluspol der gelben LED | 35 rot BCM 19 -> Vorwiderstand rote LED 100 Ω -> Pluspol der roten LED | O 33 grün BCM 13 -> Vorwiderstand grüne LED 68 Ω -> Pluspol der grünen LED | U 31 lila BCM 6 -> Vorwiderstand blaue LED 220 Ω* -> Pluspol der blauen LED | T 29 grün BCM 5 -> -> Pluspol Lautsprecher | 27 - 25 - GND 23 - 21 - 19 gelb BCM 10 -> Widerstand 1 kΩ -> Taster 0 (gelb) rechter Pol | 17 - 3.3V 15 rot BCM 22 -> Widerstand 1 kΩ -> Taster 1 (rot) rechter Pol | I 13 grün BCM 27 -> Widerstand 1 kΩ -> Taster 2 (grün) rechter Pol | N 11 lila BCM 17 -> Widerstand 1 kΩ -> Taster 3 (blau) rechter Pol | 9 grün GND -> untere blaue Masse-Linie 7 - 5 - 3 - 1 lila +3.3V -> untere rote Plus-Linie >>> die Minuspole aller LEDs und des Lautsprechers werden mit Masse verbunden >>> alle linken Pole der Taster werden mit Masse verbunden >>> alle rechten Pole der Taster werden über einen 10 kΩ-Widerstand mit der unteren Plus-Linie (3.3V) verbunden * eigentlich wäre kein Vorwiderstand nötig gewesen, um die LED zu schützen, da die Durchlassspannung 3.3 V ist, doch handelt es sich um eine Hochleistungs-LED mit 2200 Millicandela und damit wäre sie ohne Widerstand viel zu hell. Optimal ist das Ganze zwar immer noch nicht, weil ein bisschen wacklig (die Stifte sind nicht lang genug), aber schon einmal ein Fortschritt. Wenn ich jetzt mehrere Breadboards hätte (hätte, weil auch die sind noch auf dem Weg zu mir), könnte ich das Breadboard samt Schaltung einfach zur Seite legen und bei Bedarf wieder hervorkramen und müsste dann nur das Flachbandkabel anstecken.

Obwohl das Verlöten auf einer Lochrasterplatine noch besser wäre. Das Breadboard wäre wieder frei und nichts kann verrutschen oder aus Versehen herausgerupft werden, weil alles fest verlötet ist. Nachteil natürlich: die Bauteile können nicht wiederverwendet werden. Außerdem ist das Löten gegenüber dem Stecken nerviger und eventuell gemachte Fehler lassen sich viel schwieriger korrigieren.



Die LED mit den Vorwiderständen kennen wir ja bereits und auch der Lautsprecher ist ein alter Bekannter. Die Vorwiderstände habe ich so gewählt, dass alle LEDs möglichst gleich hell leuchten. Einen Vorwiderstandsrechner findet ihr im Projekt Erste Schritte mit GPIO-Ansteuerung und LED. Die Pluspole der LEDs sind über die Vorwiderstände an den Input-Block (Pin Nr 39 bis 29 (Nr. abwärts, auf dem Breadboard entsprechend von links nach rechts)) angeschlossen. Die Minuspole kommen an Masse. Das gleiche gilt für den Buzzer, der braucht aber keinen Vorwiderstand.



Neu sind die Taster am unteren Rand des Breadboards. Ich habe jeweils zwei Kontakte hochgebogen, damit ich sie überhaupt noch auf dem Breadboard unter bekomme; denn ich wollte sie gerne direkt unter den LEDs, damit die Zuordnung, welcher Taster zu welcher Farbe gehört, eindeutig ist.

Als ich mir einen Taster so angeschaut habe, war ich eigentlich davon ausgegangen, dass durch ein Tastendruck der obere mit dem unteren Teil kurzgeschlossen würde. Dem ist aber nicht so. Rechts eine kleine Grafik, wie es wirklich aussieht.

Bei diesen Print-Tastern wird die linke und die rechte Hälte kurzgeschlossen, also die näher beieinander liegenden Pins. Die Pins über den langen Weg sind von Haus aus miteinander verbunden.

Daher macht es auch nichts, dass ich die eine Seite weggebogen habe und sie jetzt in der Luft hängen.







Wenn man sich Taster-Schaltung näher annschaut (Bild rechts), dann fallen zwei Widerstände auf. Die linke Pol liegt auf Masse. Der rechte Pol geht über einen 1 kΩ-Widerstand an den GPIO-Pin und über einen zweiten 10 kΩ-Widerstand auf +3.3V.

Im Ruhezustand, also wenn der Taster nicht gedrückt ist, dann ist ist der GPIO über 11 kΩ (den 1 und den 10 kΩ-Widerstand) auf 3.3 Volt geschaltet. Das erzeugt ein ständiges High-Signal am GPIO-Pin, solange der Taster nicht gedrückt ist. Da die Spannung hier hoch gehalten wird, nennt sich das Prinzip Pullup-Schaltung. Der 10 kΩ-Widerstand ist demnach der Pullup-Widerstand.

Wird jetzt der Taste gedrückt, nimmt der Strom den Weg des geringsten Widerstands, nun also von GND über nur noch 1 kΩ zum GPIO-Pin. Das erzeugt ein Low-Signal.

Den 1 kΩ-Widerstand nennt man übrigens auch Querwiderstand. Er ist im Grunde nicht unbedingt nötig, aber er trägt dazu bei, die Schaltung stromsparender zu machen. Er begrenzt den Strom, solange der Taster gedrückt ist.

Man könnte auch eine Pulldown-Schaltung machen. Dabei wird der eine Taster-Pol an den GPIO-Port und über den Pulldown-Widerstand an GND angeschlossen und der ansere Taster-Pol an +3.3V. Dann ist das Signal bei offenem Schalter Low (GPIO liegt über Pulldown-Widerstand an GND) und wird bei Tastendruck High (Taster schließt GPIO auf +3.3V). Das entspricht zwar eher der Logik (low=aus/offen und high=ein/gedrückt), aber die Pullup-Schaltung ist störungssicherer und am anliegenden High-Signal sieht man gleich, dass der Taster richtig in der Schaltung hängt. Man muss halt nur beim Programmieren querdenken.

Was passiert eigentlich, wenn man die Widerstände weglässt und den einen Pol des Tasters direkt mit +3.3V und den anderen mit dem GPIO-Pin verbindet? Die Antwort ist: dann hängt die GPIO-Leitung bei ungedrücktem Taster in der Luft und schon bei kleinsten Berührungen oder Einstrahlungen wird ein High für den GPIO-Pin ausgegeben - oder auch nicht, dass ist dann ein Glücksspiel. Ein Pullup (oder Pulldown-) Widerstand ist also für ein wohl definierten Zustand wichtig.

Source-Code

Nachfolgend nun der Code zum Spiel. Das meiste ich bereits inline dokumentiert. Auf Besonderheiten gehe ich nach dem Listing ein.

simon-says.py (klicken, um diesen Abschnitt aufzuklappen)
# -*- encoding: utf-8 -*- # 2018 by Oliver Kuhlemann # Bei Verwendung freue ich mich über Namensnennung, # Quellenangabe und Verlinkung # Quelle: http://cool-web.de/raspberry/ import RPi.GPIO as GPIO # Funktionen für die GPIO-Ansteuerung laden from time import sleep # damit müssen wir nur noch sleep() statt time.sleep schreiben from sys import exit # um das Programm ggf. vorzeitg zu beenden from random import randint # gibt ganzzahligen Zufallswert zurück GPIO.setmode(GPIO.BCM) # die GPIO-Pins im BCM-Modus ansprechen farben=["gelb","rot","grün","blau"] # Arrays werden mit rechteckigen Klammern gef. leds=[26,19,13,6] # an welchen BCM-Pins hängen die LEDs? buzzer=5 # an welchen BCM-Pin hängt der Buzzer? tasten=[10,22,27,17] # an welchen BCM-Pins hängen die Tasten? for led in leds: # LEDS sind Output GPIO.setup(led, GPIO.OUT) GPIO.setup(buzzer, GPIO.OUT) # Buzzer ist Output for taste in tasten: # Tasten sind Input GPIO.setup(taste, GPIO.IN) freqs = [440, 576.33, 784, 880] # gespielte Töne für die Farben: a4, d4, g4, a5 pNote=0.1 # Pause zwischen Noten tGanze=.75 # Länge einer ganzen Note # Falls andere Noten gewollt sind, hier eine Auswahl: noten = {"c4":261.626, "c#4":277.183, "d4":293.665, "d#4":311.127, "e4":329.628, "f4":349.228, "f#4":369.994, "g4":391.995, "g#4":415.305, "a4":440, "a#4":466.164, "b4":493.883, "c5":523.251, "c#5":554.365, "d5":587.330, "d#5":622.254, "e5":659.255, "f5":698.456, "f#5":739.989, "g5":783.991, "g#5":830.609, "a5":880, "a#5":932.328, "b5":987.767 } def sound(freq, t): # Diese Funktion erzeugt eine bestimmte Zeit t lang dur=1.0/freq/2.0 # die Frequenz freq auf dem Buzzer-GPIO-Pin anz=int(t/dur/2) for i in range (0,anz): GPIO.output(buzzer, GPIO.HIGH) sleep (dur) GPIO.output(buzzer, GPIO.LOW) sleep (dur) def spieleNote (note, xtel): # Spielt eine Note auf dem Buzzer try: freq = noten[note] except KeyError: print "Die Note " + note + " ist nicht definiert." return t = tGanze / xtel sound (freq, t) sleep (pNote) def say(num,t): # LED und Ton für best. Zeit anschalten led=leds[num] freq=freqs[num] dur=1.0/freq/2.0 anz=int(t/dur/2) GPIO.output(led, GPIO.HIGH) # LED an for i in range (0,anz): # Ton spielen GPIO.output(buzzer, GPIO.HIGH) sleep (dur) GPIO.output(buzzer, GPIO.LOW) sleep (dur) GPIO.output(led, GPIO.LOW) # LED aus def intro(): # spielt Intro-Animation ab t=0.05 for w in range (0,3): for i in range (0,4): say (i,t) for i in range (2,-1,-1): say (i,t) def falsch(): # Falsch-Animation spielen for i in range (0,4): GPIO.output(leds[i], GPIO.LOW) GPIO.output(leds[1], GPIO.HIGH) GPIO.output(leds[2], GPIO.HIGH) sound(440,.1) GPIO.output(leds[1], GPIO.LOW) GPIO.output(leds[2], GPIO.LOW) sleep(.0025) GPIO.output(leds[0], GPIO.HIGH) GPIO.output(leds[3], GPIO.HIGH) sound(220,.1) GPIO.output(leds[0], GPIO.LOW) GPIO.output(leds[3], GPIO.LOW) sleep(.0025) for i in range (0,4): GPIO.output(leds[i], GPIO.HIGH) sound(110,1.) for i in range (0,4): GPIO.output(leds[i], GPIO.LOW) def sieg(): # Sieges-Animation spielen for i in range (0,16): GPIO.output(leds[1], GPIO.LOW) GPIO.output(leds[3], GPIO.LOW) GPIO.output(leds[0], GPIO.HIGH) GPIO.output(leds[2], GPIO.HIGH) sleep(.1) GPIO.output(leds[0], GPIO.LOW) GPIO.output(leds[2], GPIO.LOW) GPIO.output(leds[1], GPIO.HIGH) GPIO.output(leds[3], GPIO.HIGH) sleep(.1) for i in range (0,4): GPIO.output(leds[i], GPIO.LOW) sleep(0.5) GPIO.output(leds[0], GPIO.HIGH) spieleNote ("c4",8) GPIO.output(leds[1], GPIO.HIGH) spieleNote ("c4",8) GPIO.output(leds[2], GPIO.HIGH) spieleNote ("c4",8) GPIO.output(leds[3], GPIO.HIGH) spieleNote ("g4",1) for i in range (0,4): GPIO.output(leds[i], GPIO.LOW) def spieleFolge(folge,dur,pause): # spielt eine Folge mit geschwindkeit dur for ton in folge: say (int(ton), dur) sleep(pause) def getTaste(): # auf Taste warten, zuerst gedrückte Taste gilt taste=-1 z=0 # Zähler, der ggf. nach ein paar Sekunden Nichtstun verlieren lässt while 1: for t in range(0,4): # Tasten hintereinander abfragen if GPIO.input (tasten[t]) == GPIO.LOW: # Taste gedrückt? GPIO.output (leds[t],GPIO.HIGH) taste = t break # For-Schleife verlassen sleep (0.010) # ein paar ms schlafen, um CPU zu entlasten if taste >= 0: break # While-Schleife verlassen z+=1 # z*40 ms gewartet, wenn mehr als 10 Sekunden kein Tastendruck Fehler if z*0.04 > 10: falsch() print ("Es wurde lange keine Taste gedrückt. Das Programm wurde abgebrochen.") GPIO.cleanup() exit(-1) while 1: # warten bis alle Tasten wieder losgelassen anz=0 for t in range(0,4): if GPIO.input (tasten[t]) == GPIO.LOW: # LOW=gedrückt, wir nutzen einen PullUp-Widerstand anz += 1 if anz == 0: break say(taste,0.1) #sleep (0.010) for i in range (0,4): GPIO.output(leds[i], GPIO.LOW) return taste def test(): # LEDs spiegeln Status der Tasten / Eingänge wider while 1: for i in range (0,4): GPIO.output(leds[i], GPIO.input(tasten[i])) print ("BCM " + str(tasten[i]) + "=" + str(GPIO.input (tasten[i])) + " ") sleep (.1) print ("\r\n") # --- Ende Funktionen --- Beginn Hauptprogramm ------------------------------------------- intro() sleep(2.0) # test() # auskommentieren, um Schaltung zu testen, dann spiegeln die LEDs den Taster-Zustand lvl=1 folge = "" dur = 1.0 # Tonlänge zu Anfang maxLevel = 10 # Mit welchem geschafften Level hat man gewonnen? pause = .5 # Pause zwischen Tönen while lvl <= maxLevel: r = randint(0, 3) # eine Farbe per Zufall wählen folge += str(r) # print ("Level " + str(lvl) + " - Folge: " + (folge)) spieleFolge(folge, dur, pause); # Tastendrücke annehmen gedrueckt="" for i in range (0,lvl): richtig=int(folge[i]) # print ("erwartet: " + farben[richtig]) taste=getTaste() # print ("gedrückt: " + farben[taste]) if taste != richtig: falsch() print ("Leider falsch. Richtig wäre " + farben[richtig] + " gewesen. Aber du hast es immerhin in das Level " + str(lvl) + " von " + str(maxLevel) + " geschafft. Bis Bald!") GPIO.cleanup() # aufräumen exit(0) # und raus sleep (.5) # kurze Pause vor nächster Runde lvl +=1 dur *= .8 # Tonfolge wird schneller pause *= .8 # ... Pausen zwischen Tönen auch if dur < .1: # aber nicht zu schnell dur=1 if pause < .1: pause = .1 sieg() print ("Du hast alle " + str(maxLevel) + " Level geschafft! Herzlichen Glückwunsch zu dieser Meisterleistung!") GPIO.cleanup() # Programm sauber verlassen und Resourcen wieder freigeben

Wir arbeiten hier erstmal mit Arrays. Das sind Sammlungen wie Dictionarys, nur nicht nach dem "Suche nach..."-Prinzip wie Dicts, sondern nach dem Zähl-Prinzip. Das erste Element hat die Nr. 0, das 2. die Nr. 1 usw. Ohne Such-Element können wir alle Elemente hintereinander wegschreiben.

Wenn leds=[26,19,13,6] definiert ist, gibt also leds[0] 26 zurück, leds[1] 19 usw. leds[4] würde einen Fehler erzeugen, da wir etwas abrufen wollten, was es nicht gibt (nämlich das fünfte Element). Merken: wir zählen ab Null. Das korrespondiert eigentlich auch ganz gut mit dem range(0,x)-Befehl, bei dem nur bis x-1 gezählt wird. So kann man sich gut angewöhnen: range(0,Anzahl): zählen ab Null und bis Anzahl-1.

Bei unserem Senso-Programm gibt es 4 LEDs, 4 Ausgänge, 4 Taster und 4 Eingänge, symbolisiert durch 4 Farben. Wir benutzen mehrere Arrays, die alle zusammenpassen: Die Farbe von links nach rechts, wie auf dem Board, farben[] leds[] tasten[] 0 gelb 26 10 1 rot 19 22 2 grün 13 27 3 blau 6 17 Der Vorteil von Arrays wird in Verbindung mit Schleifen schnell sichtbar: for led in leds: # LEDS sind Output GPIO.setup(led, GPIO.OUT) so lassen sich schnell alle LED-GPIO-Pins auf Ausgang schalten.

Die Funktionen sound und spieleNote kennen wir schon. say(num,t) ist eine Abwandlung von sound(), bei dem auch die LED mitleuchtet.

Die Funktionen intro(), falsch() und sieg() sind für ein bisschen extra Bling-Bling zuständig und spielen kleine Animationen mit den LEDs und Tönen.

Interessant wird es wieder bei der Funktion getTaste(). Die soll auf einen Tastendruck warten. Sobald eine Taste gedrückt wird, soll die entsprechende LED samt Ton aktiv werden und das solange, bis die Taste wieder losgelassen wird. Dabei soll gelten: zuerste gedrückte Taste gilt, korrigieren gilt nicht.

Die Befehle sind soweit bekannt, bis auf break. Break sorgt dafür, dass eine Schleife, in der sich das Programm gerade befindet, augenblicklich verlassen wird und es eine Stufe "hoch" springt, in einer geschachtelten Schleife also nicht gleich ganz nach draussen, sondern nur eine Ebene weit.

In unserem Code haben wir eine For-Schleife in einer While-Schleife (while 1 heißt übrigens: mache solange 1 wahr ist. Da 1 immer wahr ist, also bis in alle Ewigkeit). Verlassen wir die For-Schleife per break finden wir uns in der While-Schleife wieder und zwar in der Zeile nach dem Ende der For-Zeile, also if taste >= 0. Aber eigentlich wollen wir ganz raus, auch aus der While-Schleife. Darum merken wir uns, dass eine Taste gedrückt wurde, indem wir taste mit der gedrückten Taste besetzen. Dann fragen wir mit if taste >= 0 ab, ob wir uns etwas gemerkt habe und falls ja, folgt gleich noch ein break zum Verlassen der jetzt gültigen (äußeren) While-Schleife.

Damit landen wir dann in der nächsten While-Schleife, die dann auf das Loslassen aller Tasten wartet und solange den Ton der zuerst gehaltenen Taste spielt (die ja gilt).

Beachtenswert in der For-Schleife ist auch die Zeile if GPIO.input (tasten[t]) == GPIO.LOW und hier das == GPIO.LOW. Da wir PullUp an unseren Tasten benutzen, heißt LOW das die Taste gedrückt wurde. Hier sollte auf jeden Fall ein klärender Kommentar in den Source-Code, damit wir später einmal nicht rätseln müssen, ob High oder Low eine gedrückte Taste bedeutet.

Die letzte Funktion namens test() kann im Hauptprogramm auskommentiert werden. Dann leuchten alle LEDs, wenn sie auf High sind, also im Normalfall. Wird dann ein Taster gedrückt, geht die LED solange aus. So kann man schnell sehen, ob etwas nicht an der Schaltung stimmt: Das folgende Hauptprogramm sollte selbsterklärend sein. Zuerst wird die Intro-Animation abgespielt. Dann ein Ton. War der richtig, kommt ein weiterer dazu, die neue Folge wird abgespielt und muss "nachgedrückt" werden. Bei einem Fehler ertönt ein Fehlerton (falsch()) und das Spiel ist benendet. Ansonsten geht das Spielchen solange, bis der definierte Maximal-Level maxLevel erreicht wurde. Dann endet das Spiel durch Sieg und nach der Sieges-Fanfare wird auch hier das Programm beendet.

Der Befehl exit() sorgt übrigens für das sofortige Verlassen des gesamten Programmes. Viele Jünger der "strukturierten Programmierung" halten so etwa für einen Frevel, ein Programm "das mittendrin aussteigt". Ich persönlich halte es allerdings für wesentlich übersichtlicher, als den Programmaufbau durch eigentlich unnötige If-Abfragen aufzublähen. Man darf nur nicht vergessen, auch hier vorher die GPIO aufzuräumen.

Ich musste feststellen, dass dieses doch im Grunde sehr einfache Spiel mir immer noch viel Spaß macht. Das mag aber vielleicht auch an meinen Kindheitserinnerungen liegen. Egal wie dem sei, euch wünsche ich natürlich auch viel Spaß als kleine Belohnung für das Aufbauen dieser doch schon etwas umfangreicheren Schaltung.