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.

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, undglBlendFunc(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)undglColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)machen aus den Farben Materialeigenschaften: Die Farbe ausglColor4f()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.
-
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: -
02Drei Farben im Takt
Laut Buchtext sollen sich die Farben der Ebenen im Dreierrhythmus abwechseln. Mit
depth % 2unddepth % 3klappt 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 mitdepth % 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: -
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 -
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 jetztresize(width, height)und wird mitresize(*screenSize)aufgerufen.float(...) / 1000.0wird 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 mitpygame.event.get()alle anstehenden Ereignisse ab.clock.tick(30)begrenzt die Bildrate, undpygame.quit()räumt beim Beenden auf. HWSURFACEentfällt, pygame 2 ignoriert das Flag ohnehin. Die Konstanten für die Kantenglättung heißen ausdrücklichpygame.GL_MULTISAMPLEBUFFERSundpygame.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 mitdepth % 3und 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()


