Inhaltsverzeichnis
Conways Spiel des Lebens
Hier werden wir Schritt für Schritt eine einfache Variante des Spiels in C entwickeln.
Als Compiler verwenden wir die GCC (aktuell die v4.8.5 auf einem RHEL7 in der x86_64 Ausprägung).
Schritt 1: Grundgerüst
File: sdl_main.c
// CharSet: ISO 8859-15 // Beschreibung - URL: https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens // Kompilieren (mit GCC): // gcc -o sdl.elf sdl_main.c // Aufruf: // ./sdl.elf #include <stdio.h> #define SPIELFELD_HOEHE_ZEILEN 10 #define SPIELFELD_BREITE_SPALTEN 10 int spielfeld[SPIELFELD_HOEHE_ZEILEN][SPIELFELD_BREITE_SPALTEN] = {0}; int anfangsgeneration_platzieren() { int anzahl_zellen = 0; // TODO: Lebendige Zellen auf dem Spielfeld (zunaechst statisch) verteilen. return anzahl_zellen; } // anfangsgeneration_platzieren() int main(int argc, char *argv[]) { int zellen_vorhanden_lebendig = 0; zellen_vorhanden_lebendig = anfangsgeneration_platzieren(); printf("Spielfeld: %i x %i (Zeilen x Spalten).\n", SPIELFELD_HOEHE_ZEILEN, SPIELFELD_BREITE_SPALTEN); printf("Anfangsgeneration platziert: %i Zellen.\n", zellen_vorhanden_lebendig); // TODO: Das Spielfeld auf dem Bildschirm anzeigen. // TODO: Alle Regeln in der Schleife anwenden. return 0; } // main() // EOF
Schritt 2: erste Ergebnisse
In diesem Schritt haben wir einige Zellen als lebendig definiert (die Funktion "anfangsgeneration_platzieren()") und die Ausgabe des Spielfeldes am Bildschirm implementiert (die Funktion "spielfeld_anzeigen()"). Außerdem haben wir eine Funktion implementiert, die Tastatureingaben liest (die "auf_tastatureingabe_warten()").
Der Quelltext kann jetzt so aussehen:
File: sdl_main.c
// CharSet: ISO 8859-15 // Beschreibung - URL: https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens // Kompilieren (mit GCC): // gcc -o sdl.elf sdl_main.c // Aufruf: // ./sdl.elf #include <stdio.h> #include <stdlib.h> #include <string.h> #define SPIELFELD_HOEHE_ZEILEN 10 #define SPIELFELD_BREITE_SPALTEN 10 int spielfeld[SPIELFELD_HOEHE_ZEILEN][SPIELFELD_BREITE_SPALTEN] = {0}; int anfangsgeneration_platzieren() { int anzahl_zellen = 0; int zeile, spalte; // TODO: Lebendige Zellen auf dem Spielfeld (zunaechst statisch) verteilen. spielfeld[1][1] = 1; spielfeld[1][2] = 1; spielfeld[1][3] = 1; spielfeld[2][3] = 1; for (zeile = 0; zeile < SPIELFELD_HOEHE_ZEILEN; zeile++) // UEber alle Zeilen von oben nach unten. { for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) // UEber alle Spalten von links nach rechts. { if (spielfeld[zeile][spalte] == 1) // Lebendige Zellen zaehlen. anzahl_zellen++; } } return anzahl_zellen; } // anfangsgeneration_platzieren() int auf_tastatureingabe_warten(char *prompt, char *zeilenbuffer, int buffer_groesse) { int zeichen_gelesen_anzahl = 0; char fake_buffer[4] = {0}; if (prompt) printf("\n%s", prompt); else printf("\n"); if (zeilenbuffer && (buffer_groesse > 1)) // Input lesen nur, wenn Buffer Platz hat. { fgets(zeilenbuffer, buffer_groesse, stdin); // Hinweis: evtl. vorhandenes '\n' wird mitgelesen. if (strlen(zeilenbuffer) > 0) // Ist der gelesene String nicht leer? { if (zeilenbuffer[strlen(zeilenbuffer)] == '\n') // Evtl. vorhandenes '\n'... zeilenbuffer[strlen(zeilenbuffer)] = '\0'; // ...beseitigen. } zeichen_gelesen_anzahl = strlen(zeilenbuffer); // Die "netto"-Laenge des gelesenen Strings "messen". } else { fgets(fake_buffer, sizeof(fake_buffer), stdin); } fflush(stdin); // Evtl. im stdin noch vorhandene Zeichen bereinigen. return zeichen_gelesen_anzahl; } // auf_tastatureingabe_warten() void spielfeld_anzeigen() { // TODO: Das Spielfeld auf dem Bildschirm anzeigen. int zeile, spalte; printf("\n"); // Oberer Rand vom Spielfeld: printf("+"); for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) { printf("-"); } printf("+\n"); // Inhalt vom Spielfeld: for (zeile = 0; zeile < SPIELFELD_HOEHE_ZEILEN; zeile++) // UEber alle Zeilen von oben nach unten. { printf("|"); for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) // UEber alle Spalten von links nach rechts. { if (spielfeld[zeile][spalte] == 1) printf("o"); else printf(" "); } printf("|\n"); } // Unterer Rand vom Spielfeld: printf("+"); for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) { printf("-"); } printf("+\n"); } // spielfeld_anzeigen() int main(int argc, char *argv[]) { int zellen_vorhanden_lebendig = 0; zellen_vorhanden_lebendig = anfangsgeneration_platzieren(); printf("Spielfeld: %i x %i (Zeilen x Spalten).\n", SPIELFELD_HOEHE_ZEILEN, SPIELFELD_BREITE_SPALTEN); printf("Anfangsgeneration platziert: %i Zellen.\n", zellen_vorhanden_lebendig); auf_tastatureingabe_warten("Enter druecken: ", NULL, 0); printf("Die Anfangsbelegung:\n"); spielfeld_anzeigen(); // TODO: Alle Regeln in der Schleife anwenden. return EXIT_SUCCESS; } // main() // EOF
Die Ausgabe am Bildschirm sollte ungefähr so aussehen:
user@rhel:/home/user/sdl> ./sdl.elf Spielfeld: 10 x 10 (Zeilen x Spalten). Anfangsgeneration platziert: 4 Zellen. Enter druecken: Die Anfangsbelegung: +----------+ | | | ooo | | o | | | | | | | | | | | | | | | +----------+ user@rhel:/home/user/sdl>
Schritt 3: Spielregeln (zunächst als Dummies) einführen
Wir erstellen zunächst eine leere Funktion spielregeln_anwenden() um sie später mit der Funktionalität zu füllen:
int spielregeln_anwenden(int anzahl_zellen_vorher) { int anzahl_zellen_nachher = 0; printf("Anzahl lebendiger Zellen VOR der Anwendung von Spielregeln: %i Zellen.\n", anzahl_zellen_vorher); // TODO: Alle Spielregeln nacheinander anwenden. // TODO: Anzahl lebenden Zellen neu ermitteln printf("Anzahl lebendiger Zellen NACH der Anwendung von Spielregeln: %i Zellen.\n", anzahl_zellen_nachher); printf("Ergebnis: "); if (anzahl_zellen_nachher > anzahl_zellen_vorher) printf("Zuwachs von %i Zellen\n", anzahl_zellen_nachher - anzahl_zellen_vorher); else if (anzahl_zellen_nachher < anzahl_zellen_vorher) printf("Verlust von %i Zellen\n", anzahl_zellen_vorher - anzahl_zellen_nachher); else printf("keine Veraenderung\n"); return anzahl_zellen_nachher; } // spielregeln_anwenden()
Die main() erweitern wir wie folgt um den Aufruf von Spielregeln:
int main(int argc, char *argv[]) { int zellen_vorhanden_lebendig = 0; char buffer_tastatureingabe[16] = {0}; zellen_vorhanden_lebendig = anfangsgeneration_platzieren(); printf("Spielfeld: %i x %i (Zeilen x Spalten).\n", SPIELFELD_HOEHE_ZEILEN, SPIELFELD_BREITE_SPALTEN); printf("Anfangsgeneration platziert: %i Zellen.\n", zellen_vorhanden_lebendig); auf_tastatureingabe_warten("Enter druecken: ", NULL, 0); printf("Die Anfangsbelegung:\n"); spielfeld_anzeigen(); // Alle Regeln in der Schleife anwenden. while (zellen_vorhanden_lebendig > 0) // Solange noch jemand lebt. { zellen_vorhanden_lebendig = spielregeln_anwenden(zellen_vorhanden_lebendig); spielfeld_anzeigen(); auf_tastatureingabe_warten("Fuer die naechste Runde Enter druecken oder STOP um zu beenden: ", buffer_tastatureingabe, sizeof(buffer_tastatureingabe)); if (strcmp(buffer_tastatureingabe, "stop") == 0) break; } if (zellen_vorhanden_lebendig > 0) printf("Endergebnis: %i Zellen sind am Leben.\n", zellen_vorhanden_lebendig); else printf("Alle sind tot.\n"); return EXIT_SUCCESS; } // main()
Um den Code etwas zu übersichtlicher zu gestalten, ersetzen wir die Werte 0 und 1 für die toten und die lebendigen Zellen durch entsprechnde Makros:
#define ZELLE_TOT 0 #define ZELLE_LEBENDIG 1
Die anfangsgeneration_platzieren() sieht nachher wie folgt aus:
int anfangsgeneration_platzieren() { int anzahl_zellen = 0; int zeile, spalte; // TODO: Lebendige Zellen auf dem Spielfeld (zunaechst statisch) verteilen. spielfeld[1][1] = ZELLE_LEBENDIG; spielfeld[1][2] = ZELLE_LEBENDIG; spielfeld[1][3] = ZELLE_LEBENDIG; spielfeld[2][3] = ZELLE_LEBENDIG; for (zeile = 0; zeile < SPIELFELD_HOEHE_ZEILEN; zeile++) // UEber alle Zeilen von oben nach unten. { for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) // UEber alle Spalten von links nach rechts. { if (spielfeld[zeile][spalte] == ZELLE_LEBENDIG) // Lebendige Zellen zaehlen. anzahl_zellen++; } } return anzahl_zellen; } // anfangsgeneration_platzieren()
Auch in der Funktion spielfeld_anzeigen() ersetzen wir:
if (spielfeld[zeile][spalte] == 1) printf("o");
…durch:
if (spielfeld[zeile][spalte] == ZELLE_LEBENDIG) printf("o");
Schritt 4: Redundanz beseitigen
In der spielregeln_anwenden() haben wir ein TODO - die Anzahl lebenden Zellen neu zu ermitteln. Dafür erstellen wir eine Funktion lebende_zellen_zaehlen():
int lebende_zellen_zaehlen() { int anzahl_zellen = 0; int zeile, spalte; for (zeile = 0; zeile < SPIELFELD_HOEHE_ZEILEN; zeile++) // UEber alle Zeilen von oben nach unten. { for (spalte = 0; spalte < SPIELFELD_BREITE_SPALTEN; spalte++) // UEber alle Spalten von links nach rechts. { if (spielfeld[zeile][spalte] == ZELLE_LEBENDIG) // Lebendige Zellen zaehlen. anzahl_zellen++; } } return anzahl_zellen; } // lebende_zellen_zaehlen()
Diese Funktion rufen wir dann in der spielregeln_anwenden(), die jetzt wie folgt aussieht:
int spielregeln_anwenden(int anzahl_zellen_vorher) { int anzahl_zellen_nachher = 0; printf("Anzahl lebendiger Zellen VOR der Anwendung von Spielregeln: %i Zellen.\n", anzahl_zellen_vorher); // TODO: Alle Spielregeln nacheinander anwenden. anzahl_zellen_nachher = lebende_zellen_zaehlen(); // Anzahl lebenden Zellen neu ermitteln. printf("Anzahl lebendiger Zellen NACH der Anwendung von Spielregeln: %i Zellen.\n", anzahl_zellen_nachher); printf("Ergebnis: "); if (anzahl_zellen_nachher > anzahl_zellen_vorher) printf("Zuwachs von %i Zellen\n", anzahl_zellen_nachher - anzahl_zellen_vorher); else if (anzahl_zellen_nachher < anzahl_zellen_vorher) printf("Verlust von %i Zellen\n", anzahl_zellen_vorher - anzahl_zellen_nachher); else printf("keine Veraenderung\n"); return anzahl_zellen_nachher; } // spielregeln_anwenden()
An dieser Stelle stellen wir fest, dass diese Zählung wir bereits in der anfangsgeneration_platzieren() gemacht haben. Diese Redundanz müssen wir jetzt beseitigen. Statt die lebenden Zellen direkt in der anfangsgeneration_platzieren() zu zählen, rufen wir dort einfach die lebende_zellen_zaehlen() auf:
int anfangsgeneration_platzieren() { int anzahl_zellen = 0; // TODO: Lebendige Zellen auf dem Spielfeld (zunaechst statisch) verteilen. spielfeld[1][1] = ZELLE_LEBENDIG; spielfeld[1][2] = ZELLE_LEBENDIG; spielfeld[1][3] = ZELLE_LEBENDIG; spielfeld[2][3] = ZELLE_LEBENDIG; anzahl_zellen = lebende_zellen_zaehlen(); return anzahl_zellen; } // anfangsgeneration_platzieren()
Schritt 5: Fehler beseitigen
Wenn wir das Programm ausführen, es fällt auf, dass das Programm sich durch die Eingabe des Worts "stop" nicht beenden läßt. Da die Prüfung, was über die Tastatur eingegeben wirde, in der Funktion main() stattfindet:
if (strcmp(buffer_tastatureingabe, "stop") == 0) break;
…wollen wir wissen, welchen Inhalt die Variable "buffer_tastatureingabe" hat. Um den Fehler zu finden, ergänzen wir die main() um die Ausgabe des o.g. Wertes:
printf("DEBUG: VAR [%s] = [%s]\n", "buffer_tastatureingabe", buffer_tastatureingabe); if (strcmp(buffer_tastatureingabe, "stop") == 0) break;
Ein kurzer Test zeigt, dass der Zeilenumbruch am eingegebenen Wort "klebt":
Fuer die naechste Runde Enter druecken oder STOP um zu beenden: stop DEBUG: VAR [buffer_tastatureingabe] = [stop ]
Der Fehler liegt offensichtlich darin, dass das Abschneiden von '\n', das wir in der auf_tastatureingabe_warten() vornehmen, nicht funktioniert:
if (strlen(zeilenbuffer) > 0) // Ist der gelesene String nicht leer? { if (zeilenbuffer[strlen(zeilenbuffer)] == '\n') // Evtl. vorhandenes '\n'... zeilenbuffer[strlen(zeilenbuffer)] = '\0'; // ...beseitigen. }
Ja, klar - das kann nicht funktionieren! Die Zeichen-Positionen innerhalb eines C-Strings fangen nicht bei 1, sondern bei 0 an. Das heißt, das Zeichen zeilenbuffer[strlen(zeilenbuffer)] liegt hinter dem letzten Zeichen im String - wir brauchen eine Position davor. Hier ist die Korrektur:
if (strlen(zeilenbuffer) > 0) // Ist der gelesene String nicht leer? { if (zeilenbuffer[strlen(zeilenbuffer) - 1] == '\n') // Evtl. vorhandenes '\n'... zeilenbuffer[strlen(zeilenbuffer) - 1] = '\0'; // ...beseitigen. }
Problem gelöst!
Schritt 6
Stand: 26.09.2018
EOF