Das Spiel Snake auf dem Arduino Gamuino

Die Hardware für die Gamuino getaufte Kombination von Arduino, Funduino Joystick Shield und Nokia 5110 Display habe ich ja bereits beim letzten mal vorgestellt.

Da war allerdings nur ein simples Testprogramm dabei, dass die Joystick-Position und Buttons auswertet und anzeigt.

Ich versprach, ein Spiel dafür zu entwickelt, und hier ist es nun. Es lag einfach zu nahe, hier Snake zu wählen, denn:

Gamuino Hardware-Erweiterung


Beim Entwickeln und Testen von der Gamuino Snake Version fehlte mir dann doch ein wenig Sound. Irgendwie vermisste ich das gewohnte "Piep", wenn die Schlange einen Punkt frisst.

Also beschloss ich, noch einen passiven Lautsprecher nachzurüsten. Der musste aber klein sein. Außerdem stellte sich die Frage: Wo anschließen?

Nach ein bisschen Kramen in der Elektronik-Grabbel-Kiste fand ich einen passenden Mikro-Lautsprecher, der noch unter die Abdeckplatte passt, und das ziemlich genau, so dass er der Abdeckplatte sogar noch zusätzlichen Halt gibt.

Die tone()-Funktion für den Arduino unterstützt glücklicherweise jeden Pin, so dass ich den Pin A4 wählen konnte. Praktischerweise gibt es einen Header mit A4 als SDA und daneben gleich Ground unten rechts auf der Platine. Auf einen Stecker musste ich allerdings verzichten, sonst hätte die Abdeckplatte nicht mehr drauf gepasst. Also wurde die Kabel direkt angelötet, auch wenn ich das eigentlich nicht gerne tue.

Snake

Wie ging eigentlich nochmal Snake? Das ist schon einige Zeit her, dass ich das ausgiebig auf meinen Nokias gespielt habe. Es gab eine Zeit, da war Nokia - wie sagt man so schön heute? - alternativlos. Auch ich war Nokia-Fan. Schon mein erstes Handy war ein Nokia 2210i. Später folgten andere Nokia, zwischendrin auch ein Alcatel und danach ein Motorola Razor. Mein letztes Nokia war ein 8210. Als das herauskam, war es Nokias Vorzeigemodell - sehr kompakt und vollgestopft mit Technik. Allerdings auch nicht ganz billig - um die 800 Mark habe ich zahlen müssen.

Also das alte Nokia 8210 wieder rausgekramt, Akku wiederbelebt, eingeschaltet und siehe da: 20 Jahre alt und funktioniert immer noch. Und es läuft Snake V.1 darauf. Und ein paar andere Spiele, aber die sind eigentlich in Sachen Spielspaß nicht der Rede wert.

Da das Nokia Snake eigentlich ziemlich perfekt ist, habe ich mich daran orientiert: Ich finde, mein Klon sieht aus wie das Original und spielt sich auch so flüssig, nur mit dem Unterschied, dass ich nicht die superkleinen Tasten des 8120 benutzen muss, sondern zwischen Joystick und Richtungstasten wählen kann, was das Spiel doch sehr viel spaßiger macht.

So macht der Klassiker wieder Spaß! Auch wenn es nur ein simples Spiel mit ganz einfachen Regeln ist, so hat es doch immer noch einen gewissen Suchtfaktor.

Ich habe in folgendem Video noch ein bisschen zum Spieledesign erklärt. Außerdem führe ich das Programm vor. Und ja, das Nokia 8210 hat auch noch eien Gastauftritt auf seine alten Tage:



Wer Snake auf seinem Gamuino spielen will... Hier ist der Source-Code für einen Arduino Uno. Viel Spaß beim Zocken!

Source-Code

//////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #include <EEPROM.h> // zum Speichern der Highscores im EEPROM #include <SPI.h> // SPI Interface für Display #include <Adafruit_GFX.h> // Grafik-Library #include <Adafruit_PCD8544.h> // für das Nokia 5110 Display #include "pitches.h" // Noten für die Melodien // welcher Button hängt an welchem Pin? #define btnA 2 #define btnB 3 #define btnC 4 #define btnD 5 #define btnE 6 #define btnF 7 #define btnK 8 #define joyX A0 #define joyY A1 #define pinSound A4 #define ButtonForDirection true // auf true setzen, damit die Buttons zur Richtungssteuerung benutzt werden // Schalter auf 5V #define center 512 // Wert Joystick Zentrum #define centerrange 200 // wie groß ist der Mittelbereich, in dem eine Joystick-Bewegung noch nicht gewertet wird? //Schalter auf 3V3 // #define center 343 // Wert Joystick Zentrum // #define centerrange 150 // wie groß ist der Mittelbereich, in dem eine Joystick-Bewegung noch nicht gewertet wird? boolean ba=false; // Zustandsspeicher Buttons boolean bb=false; boolean bc=false; boolean bd=false; boolean be=false; boolean bf=false; boolean bk=false; int jx=512; // Zustandsspeicher Joystickposition int jy=512; boolean dl=false; // Zustandsspeicher Joystick-Direction digital (left, right, up, down) boolean dr=false; boolean du=false; boolean dd=false; Adafruit_PCD8544 display = Adafruit_PCD8544(9, 10, 11, 13, 12); // Pins für SCLK, DIN, D/C, CS, RST char sf[20][11]; // Spielfeld void setup() { pinMode (btnA, INPUT_PULLUP); pinMode (btnB, INPUT_PULLUP); pinMode (btnC, INPUT_PULLUP); pinMode (btnD, INPUT_PULLUP); pinMode (btnE, INPUT_PULLUP); pinMode (btnF, INPUT_PULLUP); pinMode (btnK, INPUT_PULLUP); pinMode (joyX, INPUT); pinMode (joyY, INPUT); Serial.begin (115200); display.begin(); display.setContrast(60); display.clearDisplay(); randomSeed(analogRead(A5)); } void drawBlock (int x, int y, char d) { // Schlangensekment zeichnen // fillRect(x,y, width, height, color) if (d == 'r') display.fillRect(x*4-2,y*4-2, 4,3, BLACK); if (d == 'l') display.fillRect(x*4-1,y*4-2, 4,3, BLACK); if (d == 'd') display.fillRect(x*4-1,y*4-3, 3,4, BLACK); if (d == 'u') display.fillRect(x*4-1,y*4-2, 3,4, BLACK); } void drawPoint(int x, int y) { // Fresspunkt zeichnen display.drawPixel(x*4, y*4-2, BLACK); display.drawPixel(x*4-1, y*4-1, BLACK); display.drawPixel(x*4+1, y*4-1, BLACK); display.drawPixel(x*4, y*4, BLACK); } void clearBlock (int x, int y) { // Block auf Display löschen display.fillRect(x*4-2,y*4-2, 4,4, WHITE); } void punktSetzen(){ boolean leer = false; int x; int y; while (!leer) { x=random(1,20); y=random(1,11); if (sf[x][y] == ' ') { //hier ist noch platz leer = true; } } sf[x][y] = 'o'; drawPoint(x,y); } // Arduino Uno: 1kb EEPROM storage. void saveHighscore (int gameNum, int platz, unsigned int score) { int addrStart = 0; int addr; unsigned int score2=0; if (gameNum == 0) addrStart = 0; // snake braucht 10 level mal 2 bytes (uint) else if (gameNum == 1) addrStart = 20; addr= addrStart + platz; Serial.print ("Adresse: "); Serial.println ("Adresse: "); EEPROM.put(addr, score); } unsigned int loadHighscore (int gameNum, int platz) { // lädt Highscore aus dem EEPROM, bleibt auch nach Ausschalten erhalten int addrStart = 0; int addr; unsigned int score=0; byte b; if (gameNum == 0) addrStart = 0; // snake braucht 10 level mal 2 bytes (uint) else if (gameNum == 1) addrStart = 20; addr= addrStart + platz; EEPROM.get(addr, score); return score; } void einschaltmelodie() { // notes in the melody: int melody[] = { //NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4 NOTE_C4, NOTE_C4, NOTE_D4, NOTE_B3 // Ga-mu-i-no }; // note durations: 4 = quarter note, 8 = eighth note, etc.: byte noteDurations[] = { // 4, 8, 8, 4, 4, 4, 4, 4 4, 4, 4, 2 }; playMelody(melody, noteDurations, 4); } void playMelody( int melody[], byte noteDurations[], int anzNoten) { for (int thisNote = 0; thisNote < anzNoten ; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; tone(pinSound, melody[thisNote], noteDuration); int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); noTone(pinSound); // Tonausgabe stoppen } } void loop() { // Hauptmenü char *games[]= {"Snake","Test"}; int game=0; int gamemax=1; // Anzahl Spiele display.clearDisplay(); display.display(); int i=0; while (1) { display.clearDisplay(); display.setCursor(0,0); display.setTextSize(2); display.setTextColor(BLACK); display.println ("Gamuino"); display.setTextSize(1); display.println ("Choose Game:"); display.print (" < "); display.print (games[game]); display.println (" >"); display.println ("\nC: let's go"); display.display(); if (i==0) einschaltmelodie(); i++; if(i>1000) i =1; delay(20); if (digitalRead(btnC) == LOW) { while (digitalRead(btnC) == LOW) delay (10); // auf loslassen warten break; } if (digitalRead(btnD) == LOW) { while (digitalRead(btnD) == LOW) delay (10); // auf loslassen warten if (game>0) game--; } if (digitalRead(btnB) == LOW) { while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten if (game<gamemax) game++; } } if (game == 0) snake(); if (game == 1) test(); } void test() { // zeigt Joystick-Position und Buttons an display.setTextSize(1); display.setTextColor(BLACK); while (1) { display.clearDisplay(); display.setCursor(0,0); display.println ("Joystick Test\n"); // Zustände einlesen und in Variablen speichern ba=!digitalRead(btnA); bb=!digitalRead(btnB); bc=!digitalRead(btnC); bd=!digitalRead(btnD); be=!digitalRead(btnE); bf=!digitalRead(btnF); bk=!digitalRead(btnK); jx=analogRead(joyX); jy=analogRead(joyY); dl = (jx < center - centerrange); dr = (jx > center + centerrange); du = (jy > center + centerrange); dd = (jy < center - centerrange); if (ButtonForDirection) { if (!dl) dl=bd; if (!dr) dr=bb; if (!du) du=ba; if (!dd) dd=bc; } display.print ("X:"); display.print (jx); display.print (" Y:"); display.println (jy); display.print ("Btns: "); if (ba) display.print ("A"); if (bb) display.print ("B"); if (bc) display.print ("C"); if (bd) display.print ("D"); if (be) display.print ("E"); if (bf) display.print ("F"); if (bk) display.print ("K"); display.println(); display.print ("Dir.: "); if (dl) display.print ("left "); if (dr) display.print ("right "); if (du) display.print ("up "); if (dd) display.print ("down "); display.println(); display.display(); delay(30); } } void snake() { // Kennt man ja von Nokia Classic Phones unsigned int score=0; int level=1; unsigned int highscore[10] = {0,0,0,0,0,0,0,0,0,0}; boolean newHighscore = false; // Highscores laden for (int i=0; i < 10; i++) { highscore[i]=loadHighscore(0,i*2); // uint hat 2 Bytes } display.clearDisplay(); display.display(); display.setTextSize(2); display.setTextColor(BLACK); display.println ("Snake!"); display.setTextSize(1); display.println ("(C) 2019 by"); display.println ("O. Kuhlemann"); display.println ("www.coolweb.de"); display.println ("Press Btn. C"); display.display(); // auf Tastendruck warten while (digitalRead(btnC) == HIGH) delay (10); start: //-------------------------------------------------- // evtl. C-loslassen abwarten while (digitalRead(btnC) == LOW) delay (10); display.clearDisplay(); display.display(); while (1) { display.clearDisplay(); display.setCursor(0,0); display.println ("Scores:"); display.print (" Last: "); display.println (score); display.print (" High: "); display.println (highscore[level-1]); display.fillRect (0,24, 84,10, WHITE); display.print ("Level: < "); display.print (level); display.println (" >"); display.println ("\nC: start game"); display.display(); delay(20); if (digitalRead(btnC) == LOW) { while (digitalRead(btnC) == LOW) delay (10); // auf loslassen warten break; } if (digitalRead(btnD) == LOW) { while (digitalRead(btnD) == LOW) delay (10); // auf loslassen warten if (level>1) level--; } if (digitalRead(btnB) == LOW) { while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten if (level<10) level++; } if (digitalRead(btnF) == LOW) { while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten display.clearDisplay(); display.setCursor(0,0); display.print ("Really clear all Snake Highscores?\nA: Yes\nC: Cancel"); display.display(); // auf A oder C warten while (digitalRead(btnA) == HIGH && digitalRead(btnC) == HIGH) delay (10); if (digitalRead(btnA) == LOW) { // ja, tu es for (int i=0; i < 40; i++) EEPROM.update (i,0); display.clearDisplay(); display.setCursor(0,0); display.print ("Done.\n\nPress Reset."); display.display(); while (1) delay (1000); } } } display.clearDisplay(); display.display(); //Spielfeldgröße (in 4x4 px Quadranten) byte xmax=20; byte ymax=11; for (int i=0; i <= 20; i++) { for (int j=0; j <= 11; j++) { sf[i][j]=' '; } } // Umgrenzungen zeichnen, weil Snake daran zerschellt // drawline (x,y, x2,y2, color) display.drawLine(0,0, 83,0, BLACK); display.drawLine(0,0, 0,47, BLACK); display.drawLine(83,47, 83,0, BLACK); display.drawLine(83,47, 0,47, BLACK); // Anfangs-Snake zeichnen // Snake ist 3 px dick, Schrittweite ist 4 // Startposition und Ausfüllung in Quadranten int x=1; int y=6; char d='r'; char dbef='r'; int slen=3; String weg= ""; // gegangenen weg speichern, um verfolgen zu können, wo Schwanzende zu löschen ist // ersten drei Segmente der Anfangs-Snake for (x=1; x <= slen; x++) { sf [x][y]='#'; drawBlock(x,y, 'r'); } x=slen; weg="rr"; // ende speichern, um blöcke wieder zu löschen, wird nach jedem Löschen neu gesetzt nach gespeicherter richtung int xend=1; int yend=6; boolean eaten=false; char dend=' '; // direction schwanzende score=0; // erreichte Punkte int mult=level; // Multiplikator wegen Schwierigkeit delay(1000); punktSetzen(); while (1) { // Spiel-Schleife dbef = d; for (int w=0; w < 500/level; w++) { // Joystick-Abfrage Schleife, die auch Spielgeschwindigkeit bestimmt // Zustände einlesen und in Variablen speichern ba=!digitalRead(btnA); bb=!digitalRead(btnB); bc=!digitalRead(btnC); bd=!digitalRead(btnD); be=!digitalRead(btnE); bf=!digitalRead(btnF); bk=!digitalRead(btnK); jx=analogRead(joyX); jy=analogRead(joyY); dl = (jx < center - centerrange); dr = (jx > center + centerrange); du = (jy > center + centerrange); dd = (jy < center - centerrange); if (ButtonForDirection) { if (!dl) dl=bd; if (!dr) dr=bb; if (!du) du=ba; if (!dd) dd=bc; } // weiterlaufen, wenn nichts gedrückt // gegensätzlich zur aktuellen Laufrichtung ignorieren, das wär der sofortige // Tod und der Joystick federt evtl. zurück, das wäre gemein if (dl) { if (dbef!='r') d='l'; } else if (dr) { if (dbef!='l') d='r'; } else if (du) { if (dbef!='d') d='u'; } else if (dd) { if (dbef!='u') d='d'; } delay (1); } // eine Schrittweite in diese Richtung gehen if (d == 'r') x++; if (d == 'l') x--; if (d == 'd') y++; if (d == 'u') y--; // Außerhalb des Rahmens? if (x <1 || x >20 || y <1 || y>11) goto gameOver; // in den eigenen Schwanz gebissen? if (sf [x][y] == '#') goto gameOver; // Punkt gefressen? eaten = (sf [x][y] == 'o'); // wenn Punkt gefressen nur verlängern // wenn keinen Punkt gefressen verlängern und letztes Glied löschen // neuer Block sf [x][y]='#'; weg += d; dend=weg[0]; drawBlock(x,y,d); if (eaten) { tone(pinSound, 4000, 20); score += mult; // wir brauchen einen neuen Punkt zum fressen punktSetzen(); } else { // Schwanzende löschen sf [xend][yend]=' '; clearBlock(xend,yend); // für nächsten Durchlauf vorbereiten weg=weg.substring(1); if (dend == 'r') xend++; if (dend == 'l') xend--; if (dend == 'd') yend++; if (dend == 'u') yend--; } display.display(); } gameOver: //-------------------------------------------------- for (int i=0; i<10; i++) { tone(pinSound,200,5); delay (5); tone(pinSound,300,5); delay (5); } newHighscore=false; if (score > highscore[level-1]) { newHighscore=true; highscore[level-1] = score; saveHighscore(0, (level-1)*2, score); // ein uint (bis 65535) speichern } display.setTextSize(2); display.setTextColor(BLACK); display.setCursor(5,5); display.println(newHighscore ? "HIGH" : "GAME"); display.setCursor(5,20); display.println(newHighscore ? "SCORE!" :"OVER!"); display.fillRect(4,37, 76,8, WHITE); display.setTextSize(1); display.setCursor(5,38); display.print("Score: "); display.print(score); display.display(); // auf Tastendruck warten while (digitalRead(btnC) == HIGH) delay (10); goto start; }

Nachtrag 2019-05-03: Der fertige Gamuino

Das Projekt Gamuino ist inzwischen abgeschlossen. Mit einigen Speichertricks ist es mir gelungen die drei Spiele Snake, FlappyBird (von Huy Tr.) und Tetris (von Yo!) in den Speicher zu quetschen. Hier eine kleinen Vorführung der Games und des Endzustands der Spiele-Konsole.