Kapitel 2 · Game of Life

Game of Life in 3D mit Python und OpenGL

Das Game of Life lässt sich nicht nur auf dem Blatt Papier spielen, sondern auch im Raum. Aus 8 Nachbarn werden sage und schreibe 26, aus Quadraten leuchtende Würfel – und die Regeln müssen neu ausgetüftelt werden. Diese Version braucht OpenGL und läuft deshalb auf deinem eigenen Rechner.

Aus dem Buch · Kapitel 2 Fortgeschritten SimulationGrafik & AnimationCode & Erklärung
Nur zum Lesen OpenGL-Programm mit eigenem Fenster – zum Ausführen auf deinem Rechner mit pygame-ce und PyOpenGL.
# Game of Life in 3D – Kapitel 2 »Game of Life«, Abschnitte 2.7 bis 2.10
# aus »Coding for Fun mit Python«, portiert auf Python 3
# Läuft auf dem eigenen Rechner:  pip install pygame-ce PyOpenGL
import random

from OpenGL.GL import *
import pygame
from pygame.locals import *

livingSpace = []
livingSpaceWidth  = 12
livingSpaceHeight = 12
livingSpaceDepth  = 12
creatureSize = 60        # Fenstergröße: 12 × 60 = 720 Pixel
rotationSpeedX = 1.0     # Grad pro Bild um die x-Achse
rotationSpeedY = 1.0     # Grad pro Bild um die y-Achse


def resize(width, height):
    if height == 0:
        height = 1
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-200.0, livingSpaceWidth * 20.0 + 200.0,
            livingSpaceHeight * 20.0 + 200.0, -200.0,
            -500.0, livingSpaceDepth * 20.0 + 500.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


def init():
    glClearColor(1.0, 1.0, 1.0, 0.0)
    glClearDepth(1.0)

    # Tiefenprüfung aktivieren
    glEnable(GL_DEPTH_TEST)
    # Art der Prüfung festlegen
    glDepthFunc(GL_LEQUAL)

    # Transparenz aktivieren
    glEnable(GL_BLEND)
    # Art der Transparenzberechnung festlegen
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    # Aktivierung der Beleuchtung
    glEnable(GL_LIGHTING)
    # eine Lichtquelle erstellen
    glLightfv(GL_LIGHT0, GL_AMBIENT, [0.6, 0.6, 0.6, 1.0])
    glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.4, 0.4, 0.4, 1.0])
    glLightfv(GL_LIGHT0, GL_POSITION,
              [(livingSpaceWidth * 20.0) / 2.0,
               (livingSpaceHeight * 20.0) / 2.0, -500.0, 1.0])
    # Aktivierung der erstellten Lichtquelle
    glEnable(GL_LIGHT0)

    # Aktivierung von Materialeigenschaften
    glEnable(GL_COLOR_MATERIAL)
    # diffuses, ambientes Licht für Vorder- und Rückseite
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
    # automatische Korrektur der Normalen aktivieren
    glEnable(GL_NORMALIZE)
    # bestmöglich rendern
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)


def initLivingSpace():
    for x in range(livingSpaceWidth):
        livingSpace.append([])
        for y in range(livingSpaceHeight):
            livingSpace[x].append([])
            for z in range(livingSpaceDepth):
                if random.randint(0, 1) == 1:
                    livingSpace[x][y].append(1000)
                else:
                    livingSpace[x][y].append(0)


def rotateView():
    # die ganze Welt ein kleines Stück um ihren Mittelpunkt drehen –
    # die Drehungen summieren sich von Bild zu Bild
    cx = livingSpaceWidth * 20.0 / 2.0
    cy = livingSpaceHeight * 20.0 / 2.0
    cz = livingSpaceDepth * 20.0 / 2.0
    glTranslatef(cx, cy, cz)
    glRotatef(rotationSpeedY, 0.0, 1.0, 0.0)
    glRotatef(rotationSpeedX, 1.0, 0.0, 0.0)
    glTranslatef(-cx, -cy, -cz)


def draw():
    rotateView()
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if livingSpace[column][row][depth] > 0:
                    healthStatus = livingSpace[column][row][depth] / 1000.0
                    if depth % 2 == 0:
                        glColor4f(1.0, 0.0, 0.0, healthStatus)
                    elif depth % 3 == 0:
                        glColor4f(0.0, 1.0, 0.0, healthStatus)
                    else:
                        glColor4f(0.0, 0.0, 1.0, healthStatus)
                    x = column * 20.0
                    y = row * 20.0
                    z = depth * 20.0
                    drawCube(x, y, z, 15.0)
    glEnd()


def drawCube(x, y, z, cubeSize):
    # vordere Seitenfläche
    glNormal3f(0.0, 0.0, -1.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z)

    # hintere Seitenfläche
    glNormal3f(0.0, 0.0, +1.0)
    glVertex3f(x, y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)

    # linke Seitenfläche
    glNormal3f(-1.0, 0.0, 0.0)
    glVertex3f(x, y, z)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)

    # rechte Seitenfläche
    glNormal3f(+1.0, 0.0, 0.0)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)

    # obere Seitenfläche
    glNormal3f(0.0, +1.0, 0.0)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)

    # untere Seitenfläche
    glNormal3f(0.0, -1.0, 0.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)


def isAlive(x, y, z):
    return livingSpace[x][y][z] == 1000


def getNeighborCount(x, y, z):
    count = 0

    xpn = (x + 1) % livingSpaceWidth
    ypn = (y + 1) % livingSpaceHeight
    zpn = (z + 1) % livingSpaceDepth

    # die acht Nachbarn in der Ebene davor
    count += isAlive(x    , ypn  , z - 1)
    count += isAlive(xpn  , ypn  , z - 1)
    count += isAlive(xpn  , y    , z - 1)
    count += isAlive(xpn  , y - 1, z - 1)
    count += isAlive(x    , y - 1, z - 1)
    count += isAlive(x - 1, y - 1, z - 1)
    count += isAlive(x - 1, y    , z - 1)
    count += isAlive(x - 1, ypn  , z - 1)

    # die acht Nachbarn in derselben Ebene – wie in 2D
    count += isAlive(x    , ypn  , z)
    count += isAlive(xpn  , ypn  , z)
    count += isAlive(xpn  , y    , z)
    count += isAlive(xpn  , y - 1, z)
    count += isAlive(x    , y - 1, z)
    count += isAlive(x - 1, y - 1, z)
    count += isAlive(x - 1, y    , z)
    count += isAlive(x - 1, ypn  , z)

    # die acht Nachbarn in der Ebene dahinter
    count += isAlive(x    , ypn  , zpn)
    count += isAlive(xpn  , ypn  , zpn)
    count += isAlive(xpn  , y    , zpn)
    count += isAlive(xpn  , y - 1, zpn)
    count += isAlive(x    , y - 1, zpn)
    count += isAlive(x - 1, y - 1, zpn)
    count += isAlive(x - 1, y    , zpn)
    count += isAlive(x - 1, ypn  , zpn)

    # und die beiden direkt davor und dahinter
    count += isAlive(x, y, zpn)
    count += isAlive(x, y, z - 1)

    return count


def calculateNextGeneration():
    neighborCount = []
    for column in range(livingSpaceWidth):
        neighborCount.append([])
        for row in range(livingSpaceHeight):
            neighborCount[column].append([])
            for depth in range(livingSpaceDepth):
                neighborCount[column][row].append(getNeighborCount(column, row, depth))

    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if 6 <= neighborCount[column][row][depth] <= 11:
                    if neighborCount[column][row][depth] == 8:
                        # Geburt eines Lebewesens
                        livingSpace[column][row][depth] = 1000
                else:
                    # langsames Sterben eines Lebewesens
                    livingSpace[column][row][depth] = livingSpace[column][row][depth] / 1.5

                if livingSpace[column][row][depth] < 200:
                    livingSpace[column][row][depth] = 0


def main():
    global rotationSpeedX, rotationSpeedY
    video_flags = OPENGL | DOUBLEBUF
    screenSize = (livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize)

    pygame.init()
    pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
    pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4)
    pygame.display.set_mode(screenSize, video_flags)
    pygame.display.set_caption("Game of Life in 3D")

    initLivingSpace()
    resize(*screenSize)
    init()

    clock = pygame.time.Clock()
    while True:
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                pygame.quit()
                return
            if event.type == KEYDOWN:
                # die Pfeiltasten ändern die Drehung
                if event.key == K_UP:
                    rotationSpeedX += 0.5
                if event.key == K_DOWN:
                    rotationSpeedX -= 0.5
                if event.key == K_RIGHT:
                    rotationSpeedY += 0.5
                if event.key == K_LEFT:
                    rotationSpeedY -= 0.5

        draw()
        calculateNextGeneration()
        pygame.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    main()

Was du hier lernst

  • Listen mit drei Dimensionen
  • 26 Nachbarn im Raum
  • Würfel aus sechs Flächen
  • Normalen und Beleuchtung
  • Transparenz mit Alpha-Blending
  • Regeln durch Ausprobieren finden

Die Erklärung stammt – leicht überarbeitet für Python 3 – aus dem Buch „Coding for Fun mit Python“ von Lars Heppert, Kapitel 2, Abschnitte 2.7 bis 2.9.

»Game of Life« in 3D

Das Game of Life kannst du nicht nur in zwei, sondern auch in drei Dimensionen spielen. Wie die flache Version funktioniert – Lebensraum, Regeln, Donut-Welt und langsames Sterben –, erklärt die Seite zum Game of Life. Hier geht es um das, was die dritte Dimension verändert. Und das ist eine ganze Menge: Durch die Tiefe kommen neue Nachbarn hinzu. Zuvor gab es genau 8, jetzt sind es sage und schreibe 26.

Ein schräg gedrehter Würfel aus vielen kleinen Würfeln in verschiedenen Grautönen, manche durchscheinend
Abbildung 2.5 aus dem Buch: »Game of Life« in 3D

Die neuen Regeln

Mit 26 Nachbarn passen die alten Regeln nicht mehr. In einer zur Hälfte gefüllten Welt hat eine Zelle im Schnitt 13 lebende Nachbarn – nach den alten Regeln stürbe fast alles sofort an Überbevölkerung. Setzt man die alten Regeln ins Verhältnis zur neuen Umgebung und probiert ein wenig herum, kommt man auf folgende Regeln:

  • Eine tote Zelle mit genau 8 lebenden Nachbarn wird in der Folgegeneration neu geboren.
  • Lebende Zellen mit weniger als 6 lebenden Nachbarn sterben an Einsamkeit.
  • Eine lebende Zelle mit 6 bis 11 lebenden Nachbarn bleibt am Leben.
  • Lebende Zellen mit mehr als 11 lebenden Nachbarn sterben an Überbevölkerung.

Zunächst hatte ich versucht, die Werte einfach umzurechnen:

Wert3D = Wert2D ÷ Nachbarn2D × Nachbarn3D

Aus »genau 3 von 8« würde so »knapp 10 von 26«. Bei zufälliger Startbelegung funktionierten diese Werte aber nicht besonders gut. Deshalb habe ich die Regeln oben ausgehend von den errechneten Werten per »Trial and Error« ermittelt. Eine andere Möglichkeit wäre, die Werte mit einem genetischen Algorithmus zu suchen – wie so etwas funktioniert, zeigt das Kapitel »Evolution im Computer« mit dem genetischen Algorithmus.

Wie empfindlich solche Regeln sind, zeigen ein paar Probeläufe mit dem Programm unten: Bei einer zur Hälfte gefüllten Startwelt ist alles offen – mal stirbt sie aus, mal bleiben ein paar Dutzend Lebewesen übrig, mal wächst eine dichte Kolonie. Mehr dazu unter »Probier mal«.

Der 3D-Lebensraum

Um die dritte Dimension zu ermöglichen, fügen wir der Liste für den Lebensraum einfach eine Verschachtelungstiefe hinzu. livingSpace[x][y][z] ist jetzt eine Zelle im Raum:

def initLivingSpace():
    for x in range(livingSpaceWidth):
        livingSpace.append([])
        for y in range(livingSpaceHeight):
            livingSpace[x].append([])
            for z in range(livingSpaceDepth):
                if random.randint(0, 1) == 1:
                    livingSpace[x][y].append(1000)
                else:
                    livingSpace[x][y].append(0)

Die Welt ist 12 × 12 × 12 Zellen groß – das sind schon 1728 Zellen mit je 26 Nachbarn, also rund 45 000 Nachbarprüfungen pro Generation.

Würfel statt Quadrate

Auch beim Zeichnen ist nicht viel zu ergänzen. Um die Darstellung etwas schöner zu gestalten, zeichnen wir die Lebewesen nicht als flache Quadrate, sondern als kleine Würfel:

def draw():
    rotateView()
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if livingSpace[column][row][depth] > 0:
                    healthStatus = livingSpace[column][row][depth] / 1000.0
                    if depth % 2 == 0:
                        glColor4f(1.0, 0.0, 0.0, healthStatus)
                    elif depth % 3 == 0:
                        glColor4f(0.0, 1.0, 0.0, healthStatus)
                    else:
                        glColor4f(0.0, 0.0, 1.0, healthStatus)
                    x = column * 20.0
                    y = row * 20.0
                    z = depth * 20.0
                    drawCube(x, y, z, 15.0)
    glEnd()

Um den Würfel bunter zu machen und die Dynamik klarer herauszustellen, bekommen die Lebewesen je nach Tiefe depth eine der drei Grundfarben Rot, Grün oder Blau. Ob eine Ebene rot oder grün wird, entscheidet die Teilbarkeit ihrer Tiefe durch zwei oder drei. Wer genau nachrechnet, merkt allerdings, dass so kein gleichmäßiger Dreierrhythmus entsteht – unter »Probier mal« findest du die Variante von der Buch-CD, die es besser macht.

Sterbende Lebewesen blenden wir diesmal über die Transparenz aus: Der vierte Wert von glColor4f() ist die Deckkraft, und für die setzen wir healthStatus ein. Weil jedes Feld höchstens den Wert 1000 hat, liegt healthStatus immer zwischen 0.0 und 1.0 – genau der Bereich, den OpenGL erwartet.

Einen Würfel zeichnen wir als sechs Quadrate, eines für jede Seite. Vor jeder Seite setzt glNormal3f() die Normale, einen Vektor, der senkrecht auf der Fläche steht und nach außen zeigt. Aus ihm berechnet OpenGL, wie viel Licht die Fläche abbekommt:

    # vordere Seitenfläche
    glNormal3f(0.0, 0.0, -1.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z)

Die übrigen fünf Seiten folgen demselben Muster.

Licht, Tiefe und Transparenz

Damit die Würfel räumlich wirken, braucht die Szene deutlich mehr Vorbereitung als die flache Version. In init() wird zuerst der Hintergrund weiß, und dann kommt alles Weitere hinzu:

  • glEnable(GL_DEPTH_TEST) aktiviert die Tiefenprüfung. OpenGL sorgt dann selbst dafür, dass weiter vorn liegende Würfel die dahinterliegenden verdecken – ohne sie müssten wir selbst von hinten nach vorn zeichnen. glDepthFunc(GL_LEQUAL) legt fest, dass alles gezeichnet wird, was näher oder gleich weit entfernt ist.
  • glEnable(GL_BLEND) schaltet die Transparenz ein, und glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) bestimmt, wie die Deckkraft einer Farbe mit dem Hintergrund verrechnet wird.
  • glEnable(GL_LIGHTING) aktiviert die Beleuchtung. Dafür brauchen wir eine Lichtquelle: 60 Prozent ambientes Licht, das alles gleichmäßig aufhellt, und 40 Prozent diffuses Licht, das vom Winkel der Fläche abhängt. Die beiden Anteile sollten zusammen möglichst nicht über 100 Prozent liegen. Die Lichtquelle sitzt mittig vor dem Lebensraum, so dass du alles gut ausgeleuchtet siehst.
  • glEnable(GL_COLOR_MATERIAL) und glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) machen aus den Farben Materialeigenschaften: Die Farbe aus glColor4f() bestimmt, wie Vorder- und Rückseite das Licht zurückwerfen. OpenGL könnte für beide Seiten auch Verschiedenes festlegen – ein Spiegel etwa ist hinten matt und vorn spiegelnd.
  • glEnable(GL_NORMALIZE) bringt alle Normalen automatisch auf die Länge 1. Streng genommen bräuchten wir das nicht, weil unsere Normalen schon die Länge 1 haben. glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) bittet schließlich um die bestmögliche Darstellung.

26 Nachbarn zählen

Nun fehlt nur noch das Zählen der Nachbarn. isAlive() bekommt die dritte Koordinate dazu:

def isAlive(x, y, z):
    return livingSpace[x][y][z] == 1000

getNeighborCount() funktioniert vom Prinzip her wie in 2D, es gehen nur mehr Nachbarn in die Summe ein: zuerst die acht in der Ebene davor, dann die acht rund um die Zelle und zuletzt die acht in der Ebene dahinter – jeweils dieselben wie in der 2D-Variante, nur mit einem anderen z. Dazu kommen die beiden Nachbarn direkt vor und direkt hinter der Zelle. Das macht 8 + 8 + 8 + 2 = 26. Die Modulo-Rechnung mit zpn sorgt dafür, dass auch die Tiefe zum Donut wird: Die hinterste Ebene ist Nachbar der vordersten.

Die Folgegeneration wird fast genauso berechnet wie in 2D, nur eben mit den neuen Regeln:

                if 6 <= neighborCount[column][row][depth] <= 11:
                    if neighborCount[column][row][depth] == 8:
                        # Geburt eines Lebewesens
                        livingSpace[column][row][depth] = 1000
                else:
                    # langsames Sterben eines Lebewesens
                    livingSpace[column][row][depth] = livingSpace[column][row][depth] / 1.5

Die Degression habe ich dabei ein wenig angehoben: Sterbende Lebewesen werden durch 1.5 statt durch 1.1 geteilt und verblassen damit schneller. So wird der Blick ins Innere der 3D-Welt schneller frei, wenn Lebewesen sterben.

Denkbar ist auch, nur die sechs direkt anliegenden Nachbarn zu berücksichtigen – die, mit denen ein Würfel eine Fläche teilt. Das zeigt Listing 2.30 im Buch, und du findest es als Variante unter »Probier mal«.

Eine Welt, die sich dreht

Ein Würfel, den man nur von vorn sieht, sieht aus wie ein Quadrat. Deshalb dreht sich die ganze Welt in jedem Bild ein kleines Stück um ihren Mittelpunkt. rotateView() verschiebt dafür den Mittelpunkt in den Ursprung, dreht um die y- und die x-Achse und schiebt alles wieder zurück. Weil die Drehungen nie zurückgesetzt werden, summieren sie sich von Bild zu Bild zu einer ständigen, gemächlichen Taumelbewegung. Mit den Pfeiltasten machst du sie schneller, langsamer oder kehrst sie um.

Auf deinem Rechner starten

Im Browser gibt es kein OpenGL-Fenster, deshalb läuft diese Version auf deinem eigenen Rechner. Du brauchst Python 3 und zwei Bibliotheken:

pip install pygame-ce PyOpenGL
python main.py

pygame-ce ist die aktiv gepflegte Community-Ausgabe von pygame; importiert wird sie ganz normal mit import pygame. Escape oder das Schließen des Fensters beendet das Programm.

Probier mal

Ideen zum Weiterbauen – auf deinem eigenen Rechner mit Python und den genannten Bibliotheken.

  1. 01Die richtige Dichte

    Mit den Regeln aus dem Buch steht eine halb gefüllte Welt auf der Kippe: In zwölf Probeläufen starb sie viermal ganz aus, viermal blieben nur ein paar Dutzend Lebewesen übrig, und nur viermal wuchs eine dichte Kolonie. Mit 30 Prozent Startbevölkerung entstand dagegen jedes Mal eine Kolonie aus rund 700 Lebewesen, und ab 60 Prozent starb jedes Mal alles an Überbevölkerung. Genau das, was das Kapitel am Anfang verspricht: nicht zu viele und nicht zu wenige.

    -                 if random.randint(0, 1) == 1:
    +                 if random.random() < 0.3:
  2. 02Drei Farben im Takt

    Laut Buchtext sollen sich die Farben der Ebenen im Dreierrhythmus abwechseln. Mit depth % 2 und depth % 3 klappt das aber nicht ganz: Jede zweite Ebene wird rot, Grün taucht nur bei 3 und 9 auf. Die Datei auf der Buch-CD rechnet deshalb zweimal mit depth % 3 – so kommen Rot, Grün und Blau schön der Reihe nach.

    -                     if depth % 2 == 0:
    +                     if depth % 3 == 0:
    -                     elif depth % 3 == 0:
    +                     elif depth % 3 == 1:
  3. 03Mehr Durchblick

    In der Datei auf der Buch-CD sind auch die lebenden Würfel halb durchsichtig. Dann sieht man tiefer in den Würfel hinein, und die Farben der Ebenen mischen sich.

    -                     healthStatus = livingSpace[column][row][depth] / 1000.0
    +                     healthStatus = livingSpace[column][row][depth] / 1000.0 * 0.5   # halb durchsichtig
  4. 04Nur die sechs direkten Nachbarn – Version 4

    Statt aller 26 Nachbarn zählen nur die sechs, mit denen sich ein Würfel eine Fläche teilt – das ist Listing 2.30. Weil es so viel weniger Nachbarn sind, braucht Version 4 von der Buch-CD auch andere Regeln: Überleben bei 2 oder 3 Nachbarn, Geburt bei genau 2, und nur etwa 8 Prozent Startbevölkerung.

    -     # die acht Nachbarn in der Ebene davor
    -     count += isAlive(x    , ypn  , z - 1)
    -     count += isAlive(xpn  , ypn  , z - 1)
    -     count += isAlive(xpn  , y    , z - 1)
    -     count += isAlive(xpn  , y - 1, z - 1)
    -     count += isAlive(x    , y - 1, z - 1)
    -     count += isAlive(x - 1, y - 1, z - 1)
    -     count += isAlive(x - 1, y    , z - 1)
    -     count += isAlive(x - 1, ypn  , z - 1)
    - 
    -     # die acht Nachbarn in derselben Ebene – wie in 2D
    -     count += isAlive(x    , ypn  , z)
    -     count += isAlive(xpn  , ypn  , z)
    -     count += isAlive(xpn  , y    , z)
    -     count += isAlive(xpn  , y - 1, z)
    -     count += isAlive(x    , y - 1, z)
    -     count += isAlive(x - 1, y - 1, z)
    -     count += isAlive(x - 1, y    , z)
    -     count += isAlive(x - 1, ypn  , z)
    - 
    -     # die acht Nachbarn in der Ebene dahinter
    -     count += isAlive(x    , ypn  , zpn)
    -     count += isAlive(xpn  , ypn  , zpn)
    -     count += isAlive(xpn  , y    , zpn)
    -     count += isAlive(xpn  , y - 1, zpn)
    -     count += isAlive(x    , y - 1, zpn)
    -     count += isAlive(x - 1, y - 1, zpn)
    -     count += isAlive(x - 1, y    , zpn)
    -     count += isAlive(x - 1, ypn  , zpn)
    - 
    -     # und die beiden direkt davor und dahinter
    -     count += isAlive(x, y, zpn)
    -     count += isAlive(x, y, z - 1)
    +     # nur die sechs direkten Nachbarn – Listing 2.30
    +     count += isAlive(x    , ypn  , z)
    +     count += isAlive(xpn  , y    , z)
    +     count += isAlive(x    , y - 1, z)
    +     count += isAlive(x - 1, y    , z)
    +     count += isAlive(x, y, zpn)
    +     count += isAlive(x, y, z - 1)
    -                 if 6 <= neighborCount[column][row][depth] <= 11:
    +                 if 2 <= neighborCount[column][row][depth] <= 3:
    -                     if neighborCount[column][row][depth] == 8:
    +                     if neighborCount[column][row][depth] == 2:
    -                 if random.randint(0, 1) == 1:
    +                 if random.randint(0, 100) < 8:

Vom Buch in den Browser

Diese Seite zeigt den Code nur zum Lesen: Das Programm zeichnet mit OpenGL in ein eigenes Fenster, und das gibt es im Browser nicht. Die Logik folgt den abgedruckten Listings 2.16 bis 2.29. Was im Buch nicht abgedruckt ist – resize(), die Fenstergröße und die Drehung –, stammt aus gameOfLife_v3.py von der Buch-CD.

Python-3-Änderungen:

  • resize((width, height)) entpackte ein Tupel direkt in der Parameterliste. Das gibt es seit Python 3 nicht mehr, deshalb heißt es jetzt resize(width, height) und wird mit resize(*screenSize) aufgerufen.
  • float(...) / 1000.0 wird zu ... / 1000.0: In Python 3 liefert / immer eine Kommazahl.
  • Die Datei auf der CD begann mit import psyco; psyco.full(). Psyco war ein Beschleuniger für Python 2, den es für Python 3 nicht gibt – sein Entwickler hat danach PyPy mitgegründet. Die Zeilen entfallen.
  • Statt pygame.event.poll(), das pro Bild nur ein einziges Ereignis abholt, arbeitet die Schleife mit pygame.event.get() alle anstehenden Ereignisse ab. clock.tick(30) begrenzt die Bildrate, und pygame.quit() räumt beim Beenden auf.
  • HWSURFACE entfällt, pygame 2 ignoriert das Flag ohnehin. Die Konstanten für die Kantenglättung heißen ausdrücklich pygame.GL_MULTISAMPLEBUFFERS und pygame.GL_MULTISAMPLESAMPLES, damit sie nicht mit gleichnamigen OpenGL-Konstanten verwechselt werden können.

Weitere Änderungen:

  • Drehung um den Mittelpunkt. In der CD-Datei drehte sich die Welt um eine Kante des Würfels und schwenkte dabei teils aus dem Bild. rotateView() dreht jetzt um den Mittelpunkt.
  • Pfeiltasten. In der CD-Datei veränderten die Pfeiltasten zwei Variablen, die nirgends verwendet wurden. Jetzt steuern sie die Drehgeschwindigkeit.
  • Fenstergröße. 12 × 60 = 720 Pixel wie in Version 4 statt 12 × 80 = 960 Pixel wie in Version 3, damit das Fenster auch auf kleinere Bildschirme passt.
  • Farben und Transparenz folgen dem Buch (depth % 2, volle Deckkraft für lebende Würfel). Die CD-Datei rechnete mit depth % 3 und halber Deckkraft – beides findest du unter »Probier mal«.
  • Das ungenutzte from OpenGL.GLU import * entfällt, die Kommentare stehen auf Deutsch wie in den Listings.

Getestet ist der Code mit Python 3.14, pygame-ce 2.5 und PyOpenGL 3.1: Er lässt sich übersetzen, alle Namen sind vorhanden, und die Simulation rechnet 20 Generationen in unter einer Zehntelsekunde.

Original aus dem Buch ansehen gameOfLife_v3.py · Python 2
'''
Created on 09.08.2009

@author: Lars Heppert
'''
import psyco
psyco.full()

from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *

import random

livingSpace = []
livingSpaceWidth = 12
livingSpaceHeight = 12
livingSpaceDepth = 12
creatureSize = 80
xRotation = 0
yRotation = 0

def resize((width, height)):
    if height == 0:
        height = 1
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-200.0, livingSpaceWidth * 20.0 + 200.0, livingSpaceHeight * 20.0 + 200.0, -200.0, -500.0, livingSpaceDepth * 20.0 + 500.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

def init():
    glClearColor(1.0, 1.0, 1.0, 0.0)
    glClearDepth(1.0)
    # Tiefenpruefung aktivieren
    glEnable(GL_DEPTH_TEST)
    # Art der Pruefung festlegen
    glDepthFunc(GL_LEQUAL)
    
    # Transparenz aktivieren
    glEnable (GL_BLEND);
    # Art der Transparenzberechnung festlegen
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    # Aktivierung der Beleuchtung
    glEnable(GL_LIGHTING)
    # eine Lichtquelle erstellen
    glLightfv(GL_LIGHT0, GL_AMBIENT, [0.6, 0.6, 0.6, 1.0])
    glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.4, 0.4, 0.4, 1.0])
    glLightfv(GL_LIGHT0, GL_POSITION, [(livingSpaceWidth * 20.0)/2.0, (livingSpaceHeight * 20.0)/2.0, -500.0, 1.0])
    # Aktivierung der erstellten Lichtquelle
    glEnable(GL_LIGHT0)
    
    # Aktivierung von Materialeigenschaften
    glEnable(GL_COLOR_MATERIAL)
    # difuses, ambientes Licht für Vorder- und Rueckseite
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
    # automatische Korrektur der Normalen aktivieren
    glEnable(GL_NORMALIZE)
    # bestmoeglich rendern
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)


def initLivingSpace():
    for x in range(livingSpaceWidth):
        livingSpace.append([])
        for y in range(livingSpaceHeight):
            livingSpace[x].append([])
            for z in range(livingSpaceDepth):
                if random.randint(0, 1) == 1:
                    livingSpace[x][y].append(1000)
                else:
                    livingSpace[x][y].append(0)

def draw():
    glTranslatef(0.0, 0.0, +(livingSpaceDepth * 20.0)/2.0)
    glRotatef(1.0, 0.0, 1.0, 0.0)
    glRotatef(1.0, 1.0, 0.0, 0.0)
    glTranslatef(0.0, 0.0, -(livingSpaceDepth * 20.0)/2.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if livingSpace[column][row][depth] > 0:
                    healthStatus = float(livingSpace[column][row][depth]) / 1000.0
                    if depth % 3 == 0:
                        glColor4f(1.0, 0.0, 0.0, 0.5 * healthStatus)
                    elif depth % 3 == 1:
                        glColor4f(0.0, 1.0, 0.0, 0.5 * healthStatus)
                    else:
                        glColor4f(0.0, 0.0, 1.0, 0.5 * healthStatus)
                        
                    x = column * 20.0
                    y = row * 20.0
                    z = depth * 20.0
                    drawCube(x, y, z, 15.0)
    glEnd()
            
def drawCube(x, y, z, cubeSize):
    # front side
    glNormal3f(0.0, 0.0, -1.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z)

    # back side
    glNormal3f(0.0, 0.0, +1.0)
    glVertex3f(x, y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)
    
    # left side
    glNormal3f(-1.0, 0.0, 0.0)   
    glVertex3f(x, y, z)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)
    
    # right side        
    glNormal3f(+1.0, 0.0, 0.0)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)

    # top side
    glNormal3f(0.0, +1.0, 0.0)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)

    # bottom side
    glNormal3f(0.0, -1.0, 0.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)

def isAlive(x, y, z):
    return livingSpace[x][y][z] == 1000

def getNeighborCount(x, y, z):
    count = 0
    
    xpn = (x + 1) % livingSpaceWidth
    ypn = (y + 1) % livingSpaceHeight
    zpn = (z + 1) % livingSpaceDepth
        
    count += isAlive(x  , ypn, z - 1)
    count += isAlive(xpn, ypn, z - 1)
    count += isAlive(xpn, y, z - 1)
    count += isAlive(xpn, y - 1, z - 1)
    count += isAlive(x  , y - 1, z - 1)
    count += isAlive(x - 1, y - 1, z - 1)
    count += isAlive(x - 1, y, z - 1)
    count += isAlive(x - 1, ypn, z - 1)

    count += isAlive(x  , ypn, z)
    count += isAlive(xpn, ypn, z)
    count += isAlive(xpn, y, z)
    count += isAlive(xpn, y - 1, z)
    count += isAlive(x  , y - 1, z)
    count += isAlive(x - 1, y - 1, z)
    count += isAlive(x - 1, y, z)
    count += isAlive(x - 1, ypn, z)
    
    count += isAlive(x  , ypn, zpn)
    count += isAlive(xpn, ypn, zpn)
    count += isAlive(xpn, y, zpn)
    count += isAlive(xpn, y - 1, zpn)
    count += isAlive(x  , y - 1, zpn)
    count += isAlive(x - 1, y - 1, zpn)
    count += isAlive(x - 1, y, zpn)
    count += isAlive(x - 1, ypn, zpn)
    
    count += isAlive(x, y, zpn)
    count += isAlive(x, y, z - 1)

    return count

def calculateNextGeneration():
    neighborCount = []
    for column in range(livingSpaceWidth):
        neighborCount.append([])
        for row in range(livingSpaceHeight):
            neighborCount[column].append([])
            for depth in range(livingSpaceDepth):
                neighborCount[column][row].append(getNeighborCount(column, row, depth))
                                                   
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if 6 <= neighborCount[column][row][depth] <= 11:
                    if neighborCount[column][row][depth] == 8:
                        # creature gets born
                        livingSpace[column][row][depth] = 1000
                else:
                    # creature dies slowly
                    livingSpace[column][row][depth] = livingSpace[column][row][depth] / 1.5
                
                if livingSpace[column][row][depth] < 200:
                    livingSpace[column][row][depth] = 0
            
def main():
    video_flags = OPENGL | HWSURFACE | DOUBLEBUF
    screenSize = (livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize)
    
    pygame.init()
    pygame.display.gl_set_attribute(GL_MULTISAMPLEBUFFERS, 1)
    pygame.display.gl_set_attribute(GL_MULTISAMPLESAMPLES, 4)
    pygame.display.set_mode(screenSize, video_flags)

    initLivingSpace()
    resize(screenSize)
    init()

    frames = 0
    ticks = pygame.time.get_ticks()
    while True:
        event = pygame.event.poll()
        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
            break
        
        global xRotation, yRotation
        if event.type == KEYDOWN:
            if event.key == K_DOWN:
                xRotation -= 1
            if event.key == K_UP:
                xRotation += 1
            if event.key == K_LEFT:
                yRotation -= 1
            if event.key == K_RIGHT:
                yRotation += 1
        
        xRotation %= 360
        yRotation %= 360
        
        draw()
        calculateNextGeneration()
        pygame.display.flip()

if __name__ == '__main__': main()
Original aus dem Buch ansehen gameOfLife_v4.py · Python 2
'''
Created on 09.08.2009

@author: Lars Heppert
'''
import psyco
psyco.full()

from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *

import random

livingSpace = []
livingSpaceWidth = 12
livingSpaceHeight = 12
livingSpaceDepth = 12
creatureSize = 60
xRotation = 0
yRotation = 0

def resize((width, height)):
    if height == 0:
        height = 1
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-200.0, livingSpaceWidth * 20.0 + 200.0, livingSpaceHeight * 20.0 + 200.0, -200.0, -500.0, livingSpaceDepth * 20.0 + 500.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

def init():
    glClearColor(1.0, 1.0, 1.0, 0.0)
    glClearDepth(1.0)
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LEQUAL)
    
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    # enable Lighting and create lights
    glEnable(GL_LIGHTING)
    glLightfv(GL_LIGHT0, GL_AMBIENT, [0.6, 0.6, 0.6, 1.0])
    glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.4, 0.4, 0.4, 1.0])
    glLightfv(GL_LIGHT0, GL_POSITION, [(livingSpaceWidth * 20.0)/2.0, (livingSpaceHeight * 20.0)/2.0, -500.0, 1.0])
    glEnable(GL_LIGHT0)
    
    # enable color tracking
    glEnable(GL_COLOR_MATERIAL)
    # front material ambient and diffuse colors track glColor
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
    # enable normalization of normal vectors
    glEnable(GL_NORMALIZE)
    
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)


def initLivingSpace():
    for x in range(livingSpaceWidth):
        livingSpace.append([])
        for y in range(livingSpaceHeight):
            livingSpace[x].append([])
            for z in range(livingSpaceDepth):
                if random.randint(0, 100) < 8:
                    livingSpace[x][y].append(1000)
                else:
                    livingSpace[x][y].append(0)

def draw():
    glTranslatef(0.0, 0.0, +(livingSpaceDepth * 20.0)/2.0)
    glRotatef(1.0, 0.0, 1.0, 0.0)
    glRotatef(1.0, 1.0, 0.0, 0.0)
    glTranslatef(0.0, 0.0, -(livingSpaceDepth * 20.0)/2.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_QUADS)
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if livingSpace[column][row][depth] > 0:
                    healthStatus = float(livingSpace[column][row][depth]) / 1000.0
                    if depth % 3 == 0:
                        glColor4f(1.0, 0.0, 0.0, 0.5 * healthStatus)
                    elif depth % 3 == 1:
                        glColor4f(0.0, 1.0, 0.0, 0.5 * healthStatus)
                    else:
                        glColor4f(0.0, 0.0, 1.0, 0.5 * healthStatus)
                        
                    x = column * 20.0
                    y = row * 20.0
                    z = depth * 20.0
                    drawCube(x, y, z, 15.0)
    glEnd()
            
def drawCube(x, y, z, cubeSize):
    # front side
    glNormal3f(0.0, 0.0, -1.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z)

    # back side
    glNormal3f(0.0, 0.0, +1.0)
    glVertex3f(x, y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)
    
    # left side
    glNormal3f(-1.0, 0.0, 0.0)   
    glVertex3f(x, y, z)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(x, cubeSize + y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)
    
    # right side        
    glNormal3f(+1.0, 0.0, 0.0)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(cubeSize + x, y, z + cubeSize)

    # top side
    glNormal3f(0.0, +1.0, 0.0)
    glVertex3f(x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z)
    glVertex3f(cubeSize + x, cubeSize + y, z + cubeSize)
    glVertex3f(x, cubeSize + y, z + cubeSize)

    # bottom side
    glNormal3f(0.0, -1.0, 0.0)
    glVertex3f(x, y, z)
    glVertex3f(cubeSize + x, y, z)
    glVertex3f(cubeSize + x, y, z + cubeSize)
    glVertex3f(x, y, z + cubeSize)

def isAlive(x, y, z):
    return livingSpace[x][y][z] == 1000

def getNeighborCount(x, y, z):
    count = 0
    
    xpn = (x + 1) % livingSpaceWidth
    ypn = (y + 1) % livingSpaceHeight
    zpn = (z + 1) % livingSpaceDepth
        
    count += isAlive(x  , ypn, z)
    count += isAlive(xpn, y, z)
    count += isAlive(x  , y - 1, z)
    count += isAlive(x - 1, y, z)
    
    count += isAlive(x, y, zpn)
    count += isAlive(x, y, z - 1)

    return count

def calculateNextGeneration():
    neighborCount = []
    for column in range(livingSpaceWidth):
        neighborCount.append([])
        for row in range(livingSpaceHeight):
            neighborCount[column].append([])
            for depth in range(livingSpaceDepth):
                neighborCount[column][row].append(getNeighborCount(column, row, depth))
                                                   
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            for depth in range(livingSpaceDepth):
                if 2 <= neighborCount[column][row][depth] <= 3:
                    if neighborCount[column][row][depth] == 2:
                        # creature gets born
                        livingSpace[column][row][depth] = 1000
                else:
                    # creature dies slowly
                    livingSpace[column][row][depth] = livingSpace[column][row][depth] / 1.5
                
                if livingSpace[column][row][depth] < 200:
                    livingSpace[column][row][depth] = 0
            
def main():
    video_flags = OPENGL | HWSURFACE | DOUBLEBUF
    screenSize = (livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize)
    
    pygame.init()
    pygame.display.gl_set_attribute(GL_MULTISAMPLEBUFFERS, 1)
    pygame.display.gl_set_attribute(GL_MULTISAMPLESAMPLES, 4)
    pygame.display.set_mode(screenSize, video_flags)

    initLivingSpace()
    resize(screenSize)
    init()

    while True:
        event = pygame.event.poll()
        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
            break
        
        global xRotation, yRotation
        if event.type == KEYDOWN:
            if event.key == K_DOWN:
                xRotation -= 1
            if event.key == K_UP:
                xRotation += 1
            if event.key == K_LEFT:
                yRotation -= 1
            if event.key == K_RIGHT:
                yRotation += 1
        
        xRotation %= 360
        yRotation %= 360
        
        draw()
        calculateNextGeneration()
        pygame.display.flip()

if __name__ == '__main__': main()