| Weitere Artikel aus dem Elo-Magazin |
Snake
Von Mirko Horstmann (www.mirkokosmos.net)
Neben dem Urvater aller Videospiele gibt es noch einige Klassiker, die sich auf der Spielekonsole umsetzen lassen. Das hier in einer einfachen Variante vorgestellte Spiel "Snake" taucht seit Ende der 70er Jahre in den verschiedensten Ausprägungen immer wieder auf, beispielsweise als Dreingabe zu Betriebssystemen oder Handys. Ziel des Spiels ist es, eine Schlange so über den Bildschirm zu steuern, dass sie nicht mit sich selbst oder dem Spielfeldrand kollidiert und dabei die zufällig auf dem Bildschirm platzierten Äpfel einsammelt. Jeder aufgesammelte Apfel verlängert dabei die Schlange um ein Stück, so dass das Spiel mit der Zeit immer schwieriger wird.
| Ganzen Artikel lesen... | ![]() |

Die Steuerung der Schlange wäre mit Tasten vermutlich einfacher als mit den an der Pingpong-Konsole vorhandenen Reglern, so dass man sich durchaus überlegen könnte, die Hardware um einige Tasten zu erweitern - Lötpunkte mit Verbindungen zu I/O-Pins des ATMega8 sind auf der Platine ja vorhanden. Das hier vorgestellte Spiel kommt aber mit der Standardausstattung aus, wobei ein kurzer Dreh des rechten Reglers um mehr als 45 Grad in die eine oder andere Richtung die Bewegungsrichtung im bzw. gegen den Uhrzeigersinn ändert. Es ist darauf zu achten, dass nach der Richtungsänderung der Regler sofort wieder in die Mittelstellung gebracht wird, damit die Schlange sich nicht im Kreis dreht und so mit sich selbst kollidiert. Damit der Spieler weiß, wo sich die Mitte befindet, gibt es vor Anfang des Spiels eine kurze "Kalibrierungsphase", in der man diese suchen muss. Dazu bewegt man den Regler in Richtung der dargestellten Pfeile, bis diese durch eine Linie ersetzt werden, die man möglichst in der Mitte des Bildschirms platziert. Ist die Linie etwa anderthalb Sekunden ununterbrochen zu sehen gewesen, dann geht das Spiel los: fortan erscheint immer ein Apfel (blinkend) auf dem Bildschirm, den es einzusammeln gilt. Jeder Apfel bringt einen Punkt, nach Ende des Spiels erfolgt eine Punkteanzeige.
YouTube-Video: Snake on a Pongpong
Programm
Die Ansteuerung des Bildschirms erfolgt, wie schon aus dem Laufschrift-Programm bekannt, in einer Timer-gesteuerten Interruptroutine, die sich die Pixeldaten aus einem globalen Array ("leds") holt. Zusätzlich gibt es ein Array, in dem der jeweils aktuelle Apfel eingetragen wird. Dieser wird bei jedem vierten Bildschirmaufbau mit in die Anzeige kopiert, so dass die Äpfel blinkend erscheinen. Die Position aller zur Schlange gehörenden Pixel sind außerdem in einem Ringpuffer ("snake") gespeichert, damit sie korrekt wieder vom Bildschirm gelöscht werden können.
Der Analog-Digital-Konverter arbeitet bei diesem Programm im sogenannten Free-Running-Mode, liefert also ständig einen 10-bit-Wert, der zu Anfang der Hauptschleife aus den Datenregistern ADCL und ADCH ausgelesen wird.
Das Programm unterstützt den aus dem Originalspiel bekannten Start durch Münzeinwurf. Die Funktion "sleepNow" ermöglicht dazu vor dem Standby nach Spielende am entsprechenden Eingang (INT0) den externen Interrupt, der durch Kontakt der beiden Drähte vor dem nächsten Spiel ausgelöst wird. Nach dem Aufwachen wird der Interrupt deaktiviert und INT0 wieder auf Ausgang geschaltet.
Entwickelt wurde das Programm mit gcc-avr.
Download: C-Quellcode und Hexfile
/*
* main.c
*
* Snake-Spiel für die Ping-Pong-Konsole des Franzis-Verlags.
*
* Der
Sourcecode und das Hexfile dürfen frei verwendet werden.
* Nutzung erfolgt auf eigene Gefahr.
*
* Autor: Mirko Horstmann
*
* Teile des Programms (z.B. Interruptroutine für den
Display-Aufbau)
* wurden von Sascha Bader auf http://www.elo-web.de zur Verfügung
* gestellt.
*
*/
/* -----------------------------------------
* Defines (Präprozessor Makros)
*
-----------------------------------------*/
#define F_CPU 8000000UL /*
CPU Takt (für delay-Routine) */
#define WIDTH 12 /* Breite des
Displays */
#define HEIGHT 10 /* Höhe des Displays */
#define RIGHT
0
#define DOWN 1
#define LEFT 2
#define UP 3
/* -----------------------------------------
*
Includes
* -----------------------------------------*/
#include <inttypes.h>
/* Definition der Datentypen uint8_t usw. */
#include <avr/interrupt.h>
/* Interruptbehandlungsroutinen (für Timerinterrupt) */
#include <util/delay.h>
/* Definition der Verzögerungsfunktionen (_delay_ms) */
#include
<avr/pgmspace.h> /* Hilfsfunktionen um Daten aus dem Flash zu lesen */
#include <avr/sleep.h> /* Sleep-Modi des ATMega8 */
#include
<stdlib.h>
// Globale Variablen
volatile uint16_t leds[WIDTH]; // Inhalt der LED-Matrix
volatile uint8_t col = 0; // Spalte
volatile uint8_t onOff = 0; // Schalter für die Einblendung
der Edelsteine
volatile uint16_t apples[WIDTH]; // Karte mit eingezeichnetem Edelstein
uint8_t
dirX[] = {1, 0, -1, 0}; // vier Richtungen...
uint8_t dirY[] = {0, 1, 0, -1};
uint8_t dir;
struct
{
unsigned x:4;
unsigned y:4;
} snake[120]; // Ringpuffer für die Pixel, die zur
Schlange gehören
uint8_t snakeHead; // Position des ersten Schlangenpixels
uint8_t snakeTail; //
Position des letzten Schlangenpixels
uint8_t snakeLength; // Länge der Schlange
uint8_t
nextSnakeHead; // Zwischenspeicher für die nächste Kopf-Position
uint8_t endGame;
uint16_t
potiValue; //10-bit-Wert des rechten Potentiometers
// Ziffern für die Punkteanzeige
static
const uint8_t PROGMEM digits[10][4] = {
{
0b01111100,
0b10000010,
0b10000010,
0b01111100
},{
0b00000000,
0b00000100,
0b11111110,
0b00000000
},{
0b11000100,
0b10100010,
0b10010010,
0b10001100
},{
0b01000100,
0b10010010,
0b10010010,
0b01101100
},{
0b00011110,
0b00010000,
0b00010000,
0b11111110
},{
0b01011110,
0b10010010,
0b10010010,
0b01100010
},{
0b01111100,
0b10010010,
0b10010010,
0b01100100
},{
0b00000010,
0b11100010,
0b00011010,
0b00000110
},{
0b01101100,
0b10010010,
0b10010010,
0b01101100
},{
0b01001100,
0b10010010,
0b10010010,
0b01111100
}
};
/*
--------------------------------------------------------------------
* Funktion sleepNow
*
*
Legt den ATMega8 schlafen (Standby-Modus)
*
--------------------------------------------------------------------
*/
void sleepNow()
{
// Bildschirm löschen, damit keine LEDs anbleiben und Strom verbrauchen.
uint8_t i;
for (i = 0; i<WIDTH; i++) {
leds[i] = 0;
apples[i] = 0;
}
_delay_ms(250); // Warten bis der leere Bildschirm von der Interruptroutine übernommen wurde
// Den Pin INT0 auf Eingang stellen, damit der Münzeinwurf während des Standby funktioniert
DDRD &= ~(1<<DDD2);
MCUCR |= (1<<SE) | (1<<SM2) | (1<<SM1); //Sleep Enable, STAND-BY-Modus
GICR |= (1<<INT0); //Externen Interrupt bei "low" auf INT0 erlauben
sleep_mode(); // In den
gewählten Sleep-Modus gehen
//Nach dem Aufwachen den externen Interrupt wieder ausstellen
GICR &= ~(1<<INT0);
//INT0 wieder auf Ausgang
DDRD |= (1<<DDD2);
}
/*
-----------------------------------------------------------------------
* Funktion initGame
*
* Initialisiert das Spielfeld und die restlichen Variablen und lässt den
* Spieler die Mitte des
Einstellbereichs suchen
*
-----------------------------------------------------------------------
*/
void initGame() {
uint16_t caliCount = 0;
// Poti "kalibrieren"
while(caliCount<1000) {
// Wert des ADC auslesen
potiValue = ADCL;
potiValue += (ADCH<<8);
uint8_t i;
if (506<potiValue && potiValue<517) {
caliCount++;
for (i = 0; i<WIDTH; i++) leds[i] = (1<<(potiValue-507));
} else {
caliCount = 0;
if (potiValue<507) {
leds[0] = 0b0000000000;
leds[1] = 0b0000000000;
leds[2] = 0b0001000000;
leds[3] = 0b0011000000;
leds[4] = 0b0111000000;
leds[5] =
0b1111111111;
leds[6] = 0b1111111111;
leds[7] = 0b0111000000;
leds[8] = 0b0011000000;
leds[9] = 0b0001000000;
leds[10] = 0b0000000000;
leds[11] = 0b0000000000;
} else if (potiValue>516) {
leds[0] = 0b0000000000;
leds[1] = 0b0000000000;
leds[2] = 0b0000001000;
leds[3] =
0b0000001100;
leds[4] = 0b0000001110;
leds[5] = 0b1111111111;
leds[6] = 0b1111111111;
leds[7] = 0b0000001110;
leds[8] = 0b0000001100;
leds[9] = 0b0000001000;
leds[10] = 0b0000000000;
leds[11] = 0b0000000000;
}
}
_delay_ms(2);
}
uint8_t i;
for (i=0; i<12; i++) {
leds[i] = 0;
apples[i] = 0;
}
// Den ersten Apfel auf dem Spielfeld platzieren
uint8_t x;
uint8_t y;
do x = rand()%12; while (x==snake[snakeHead].x);
do y = rand()%10; while
(y==snake[snakeHead].y);
apples[x] |= 1<<(y);
dir = 2;
snakeHead = 4;
snakeTail
= 0;
leds[7] = 0b0000000000010000;
leds[8] = 0b0000000000010000;
leds[9] =
0b0000000000010000;
leds[10] = 0b0000000000010000;
leds[11] = 0b0000000000010000;
snake[0].x = 11;
snake[0].y = 4;
snake[1].x = 10;
snake[1].y = 4;
snake[2].x = 9;
snake[2].y = 4;
snake[3].x = 8;
snake[3].y = 4;
snake[4].x = 7;
snake[4].y = 4;
snakeLength = 5;
endGame = 0;
}
int
main(void)
{
cli(); // Interrupts sperren
// Ports konfigurieren (Ein-/Ausgänge)
DDRC =
0x0f; // PORTC als AD-Eingang
DDRB = 0xff; // PORTB = Output
DDRD = 0xff; //
PORTD = Output
/*---------------------------------------------------------------------------
* 8-Bit Timer
TCCR0 für das Multiplexing der LEDs initialisieren
* Es wird ca. alle 2 Mikrosekunden ein
Overflow0 Interrupt ausgelöst
* Berechnung: T = Vorteiler * Wertebereich Zähler / Taktfreuenz
* = 64 * 256 / ( 8000000 Hz ) = 2,048 ms
*---------------------------------------------------------------------------*/
TCCR0 |=
(1<<CS01) | (1<<CS00); // 8-bit Timer mit 1/64 Vorteiler
TIFR |= (1<<TOV0);
// Clear overflow flag (TOV0)
TIMSK |= (1<<TOIE0);
// timer0 will create overflow interrupt
sei();
// Interrupts erlauben
// ADC konfigurieren
// Referenz ist die Spannung an AVCC,
Eingang an Pin 7 von Port A
ADMUX = 0b01000111;
// Free-Running-Mode, Prescaler 128
(=62,5kHz Samplerate)
ADCSRA = 0b11100111;
initGame();
while(1)
{
if (endGame) {
sleepNow();
initGame();
}
// Wert des ADC auslesen
potiValue = ADCL;
potiValue += (ADCH<<8);
if (potiValue<256) {
if (dir==UP) dir = RIGHT;
else dir++;
} else if (potiValue>768) {
if (dir==RIGHT) dir =
UP;
else dir--;
}
nextSnakeHead =
(snakeHead+1)%120;
snake[nextSnakeHead].x = snake[snakeHead].x+dirX[dir];
snake[nextSnakeHead].y = snake[snakeHead].y+dirY[dir];
if (snake[nextSnakeHead].x<0 ||
snake[nextSnakeHead].x>11 ||
snake[nextSnakeHead].y<0 ||
snake[nextSnakeHead].y>9 ||
(leds[snake[nextSnakeHead].x] & (1<<snake[nextSnakeHead].y))) {
// Game Over!
uint8_t pointDigits[3];
uint8_t temp =
snakeLength-5;
uint8_t i, j;
uint8_t digCount = 0;
uint8_t noOfOnes = 0;
for (i = 0; i<3; i++) {
pointDigits[i] = temp%10;
temp -= pointDigits[i];
temp /= 10;
if (temp==0) break;
}
digCount = i+1;
for (i = 0; i<WIDTH; i++) {
leds[i] = 0;
apples[i] = 0;
}
_delay_ms(1000);
for (i = 0; i<digCount; i++) {
if (pointDigits[i]==1) noOfOnes++;
}
// Punktezahl in
der Mitte platzieren
uint8_t textX =
11-(12-(digCount*4-noOfOnes*2+digCount-1))/2;
for (i = 0; i<digCount; i++) {
if (pointDigits[i]==1) {
for (j = 3; j>1;
j--) {
leds[textX--] = pgm_read_byte(&digits[1][j-1]);
}
} else {
for (j = 4; j>0; j--) {
leds[textX--] =
pgm_read_byte(&digits[pointDigits[i]][j-1]);
}
}
leds[textX--] = 0;
}
_delay_ms(10000);
endGame = 1;
} else {
snakeHead = nextSnakeHead;
leds[snake[snakeHead].x] |=
1<<snake[snakeHead].y;
if (apples[snake[snakeHead].x]&(1<<snake[snakeHead].y))
{
apples[snake[snakeHead].x] &= ~(1<<snake[snakeHead].y);
snakeLength++;
// neuer Apfel
uint8_t x;
uint8_t y;
do {
x = rand()%12;
y = rand()%10;
} while (leds[x]&(1<<y));
apples[x] |= 1<<(y);
}
else {
leds[snake[snakeTail].x] &= ~(1<<snake[snakeTail].y);
snakeTail = (snakeTail+1)%120;
}
_delay_ms(500);
}
}
return 0;
}
/*
-------------------------------------------------------------------------
* Interrupt Routine
zum Aufwachen
* -------------------------------------------------------------------------
*/
SIGNAL (SIG_INTERRUPT0)
{
//Hallo wach!
}
/*
-------------------------------------------------------------------------
* Interrupt Routine
für Display-Darstellung
*
* Gibt nacheinander alle Spalten mit LED-Daten aus.
* Dazu wird
mittels der Schieberegister die aktuelle Spalte
* ausgewählt und dann das Bitmuster derselben
auf die Ports
* gegeben.
* Beim nächsten Interrupt ist dann die nächste Spalte dran.
*
-------------------------------------------------------------------------*/
SIGNAL
(SIG_OVERFLOW0)
{
uint16_t ledval;
uint8_t portcout;
uint8_t portdout;
cli(); /* Interrupts verbieten */
/*--------------------------------------------------
* Aktuelle Spalte ermitteln
*--------------------------------------------------*/
col++;
if (col == 12)
{
col = 0;
}
/*--------------------------------------------------
* Ports
initialisieren
*--------------------------------------------------*/
PORTD = 0;
PORTB =
0;
PORTC = 0;
/*---------------------------------------------------
* Eine einzelne
0 durch die Schiebergister schieben
*---------------------------------------------------*/
if ( col == 0 )
{
PORTB &= ~(1 << 4); /* Bei der ersten Spalte eine
0 ausgeben (PB4 = 0) */
/* Diese 0 geht auf die Reise
durch die Schieberegister */
if (onOff++>=4) onOff = 0; /* Alle vier Durchgänge die
Darstellung des aktuellen */
/* Diamanten ein- bzw.
ausstellen */
}
else
{
PORTB |= (1 << 4); /* Danach Einsen
hinterherschicken (PB4 = 1) */
}
/*---------------------------------------------------
* Impulse für die Schieberegister generieren
*---------------------------------------------------*/
PORTB |= (1 << 3); /* PB3 = 1
(cl) */
PORTB &= ~(1 << 3); /* PB3 = 0 (!cl) */
PORTB |= (1 << 2);
/* PB2 = 1 (str) */
PORTB &= ~(1 << 2); /* PB2 = 0 (!str) */
/*---------------------------------------------------
* Daten der Spalte holen und auf die
Ports verteilen
*---------------------------------------------------*/
ledval =
leds[col];
if (onOff==0) ledval |= apples[col]; // ggf. Diamanten anzeigen
else
ledval &= ~apples[col]; // ...oder löschen
portdout = ledval & 0xff; /* low
byte */
portcout = portdout & 0x0f; /* low nibble */
portdout = portdout &
0xf0; /* high nibble */
PORTD = portdout & 0xf0;
PORTC = portcout & 0x0f;
PORTB = (ledval >> 8) & 0x03; /* high byte */
sei();
/* Interrupts wieder erlauben */
}












