Was du hier lernst
- Listen von Listen (2D-Raster)
- Modulo und negative Indizes
- Regeln als if-Abfragen
- Zwei Durchläufe pro Generation
- Farben aus Zahlenwerten
- Maus- und Tastatur-Ereignisse
So funktioniert das Spiel des Lebens
Der Mathematiker John Horton Conway erfand das »Game of Life« 1970. Es ist ein zellulärer Automat: ein Raster aus Zellen, die entweder leben oder tot sind. Niemand spielt aktiv mit, man schaut nur zu, wie sich die Welt von Generation zu Generation entwickelt. Das Programm dafür ist erstaunlich kurz.
1. Der Lebensraum: eine Liste von Listen
Das Raster ist eine Liste mit 100 Spalten, und jede Spalte ist wieder eine Liste mit 60 Zellen. livingSpace[x][y] ist dann die Zelle in Spalte x und Zeile y:
for x in range(livingSpaceWidth):
livingSpace.append([])
for y in range(livingSpaceHeight):
if random.randint(0, 1) == 1:
livingSpace[x].append(1000)
else:
livingSpace[x].append(0)
Jede Zelle bekommt zufällig den Wert 1000 (lebendig) oder 0 (tot). Warum 1000 und nicht einfach 1? Das hat mit dem langsamen Sterben zu tun – dazu gleich mehr.
2. Die Regeln
Für jede Zelle zählt nur eines: Wie viele ihrer acht Nachbarn leben?
| Lebende Nachbarn | Was passiert |
|---|---|
| genau 3 | Eine tote Zelle wird geboren, eine lebende bleibt am Leben. |
| genau 2 | Die Zelle bleibt, wie sie ist. |
| weniger als 2 | Die Zelle stirbt an Einsamkeit. |
| mehr als 3 | Die Zelle stirbt an Überbevölkerung. |
Im Code sind das nur ein paar Zeilen:
if 2 <= neighborCount[column][row] <= 3:
if neighborCount[column][row] == 3:
livingSpace[column][row] = 1000
else:
livingSpace[column][row] = livingSpace[column][row] / 1.1
Python erlaubt verkettete Vergleiche wie 2 <= n <= 3 – das liest sich wie in der Mathematik.
3. Nachbarn zählen – und eine Welt ohne Rand
getNeighborCount fragt die acht Nachbarn der Reihe nach ab. isAlive liefert True oder False, und weil True in Python als 1 zählt, kann das Programm die Ergebnisse einfach addieren.
Spannend wird es am Rand. Was ist der linke Nachbar einer Zelle in Spalte 0? Das Programm fragt x - 1, also Spalte −1 – und Python versteht negative Indizes als »von hinten gezählt«. Spalte −1 ist die letzte Spalte. Für den rechten Rand hilft der Modulo-Operator: (x + 1) % livingSpaceWidth springt von der letzten Spalte zurück auf 0.
Die Welt ist ein Donut
Weil der linke Rand mit dem rechten und der obere mit dem unteren verbunden ist, hat die Welt keine Grenzen. Mathematisch ist sie ein Torus – die Form eines Donuts. Ein Gleiter, der rechts hinausfliegt, kommt links wieder herein.
4. Erst zählen, dann ändern
calculateNextGeneration arbeitet in zwei Durchläufen. Zuerst zählt es für alle Zellen die Nachbarn und merkt sich das Ergebnis in neighborCount. Erst danach ändert es die Zellen.
Würde das Programm sofort ändern, sähe die Zelle rechts daneben bereits die neue Generation ihres Nachbarn, die Zelle links aber noch die alte. Die Regeln verlangen jedoch, dass alle Zellen gleichzeitig umschalten. Die Hilfsliste sorgt genau dafür.
5. Langsames Sterben
Im Original sterben Zellen sofort. Diese Version aus dem Buch lässt sie verblassen: Statt auf 0 zu fallen, wird ihr Wert bei jeder Generation durch 1,1 geteilt, bis er unter 200 fällt. Lebendig ist eine Zelle nur mit genau 1000 – Sterbende zählen also nicht mehr als Nachbarn, bleiben aber noch eine Weile sichtbar.
Die Farbe hängt direkt am Wert: healthStatus liegt zwischen 0 und 1, und daraus wird die Rotstufe.
healthStatus = livingSpace[column][row] / 1000.0
color = (int(255 * healthStatus), 0, 0)
So entstehen die glimmenden Spuren hinter jedem Muster.
6. Mitspielen
Neu im Browser: Ein Klick oder Wisch setzt Zellen auf 1000. Das Programm rechnet die Position auf der Leinwand mit einer Ganzzahldivision in Spalte und Zeile um – event.x // creatureSize. Die Leertaste hält die Zeit an, damit du in Ruhe ein Muster zeichnen kannst.
Muster zum Nachzeichnen
Pausiere das Spiel, leere das Feld mit »c« und zeichne drei Zellen in eine Reihe: Das ist ein Blinker, der zwischen waagerecht und senkrecht wechselt. Vier Zellen im Quadrat bilden einen Block, der sich nie verändert. Unter »Probier mal« findest du den Gleiter.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Ohne langsames Sterben – Version 1 aus dem Buch
In der ersten Fassung im Buch sterben Zellen sofort. Die Muster sind dann schärfer, die Spuren verschwinden. Vergleiche, wie sich die Wirkung ändert.
- livingSpace[column][row] = livingSpace[column][row] / 1.1 + livingSpace[column][row] = 0 -
02Ein Gleiter auf leerem Feld
Der Gleiter ist das berühmteste Muster: fünf Zellen, die sich alle vier Generationen um ein Feld schräg verschieben. Weil die Welt an den Rändern verbunden ist, taucht er auf der anderen Seite wieder auf.
- initLivingSpace() - running = True + initLivingSpace() + clearLivingSpace() + for x, y in [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]: + livingSpace[x + 10][y + 10] = 1000 + running = True -
03Dünn besiedelt
Statt jede zweite Zelle zu beleben, würfelt das Programm jetzt nur jede siebte. Aus der dünnen Startbevölkerung entwickeln sich einzelne Inseln und oft auch Gleiter.
- if random.randint(0, 1) == 1: + if random.random() < 0.15: -
04Grünes Leben
Die Farbe entsteht aus dem Zustand der Zelle: 1000 heißt voll lebendig, kleinere Werte bedeuten »stirbt gerade«. Hier wird aus dem Wert ein Grün mit einem Hauch Blau.
- color = (int(255 * healthStatus), 0, 0) + color = (0, int(255 * healthStatus), int(140 * healthStatus)) -
05Zeitlupe
Vier Generationen pro Sekunde statt fünfzehn – jetzt kannst du jedem einzelnen Schritt folgen und die Regeln an einzelnen Zellen nachprüfen.
- await screen.frame(15) + await screen.frame(4)
Vom Buch in den Browser
Im Buch zeichnet das Game of Life mit OpenGL: Jede Zelle ist ein Viereck aus vier Eckpunkten, der Rotwert wird per glColor4f gesetzt. Hier übernimmt screen.rect diese Arbeit. Die Spiellogik – initLivingSpace, isAlive, getNeighborCount, calculateNextGeneration – ist unverändert aus Version 2 übernommen.
| Im Buch (pygame + OpenGL) | Hier (c4f) |
|---|---|
glColor4f(healthStatus, 0.0, 0.0, 1.0) |
Farbe (int(255 * healthStatus), 0, 0) |
glBegin(GL_QUADS) und viermal glVertex3f(...) |
screen.rect(x, y, 9, 9, farbe) |
glOrtho(...) in resize() |
entfällt, die Leinwand zählt in Pixeln |
pygame.event.poll() |
screen.events() |
pygame.display.flip() |
await screen.frame(15) |
Python-3-Änderungen:
- Die Funktion
resize((width, height))entpackte ein Tupel direkt in der Parameterliste. Das ging nur in Python 2 und wurde in Python 3 abgeschafft. Die Funktion entfällt hier ohnehin. livingSpace[column][row] / 1.1rechnet in beiden Versionen mit Kommazahlen, daran ändert sich nichts. Bei der Umrechnung von Mauspositionen ist dagegen//wichtig: In Python 3 liefert/immer eine Kommazahl,//teilt ganzzahlig.
Neu gegenüber dem Buch sind die Ereignisse: Zellen per Maus setzen, Pause, neu würfeln und leeren. Im Original beendete die Escape-Taste das Programm, hier übernimmt das der Knopf »Stopp«. Die Bildrate ist auf 15 Generationen pro Sekunde begrenzt, damit man den Mustern folgen kann.
Original aus dem Buch ansehen gameOfLife_v1.py · Python 2
'''
Created on 09.08.2009
@author: Lars Heppert
'''
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *
import random
livingSpace = []
livingSpaceWidth = 100
livingSpaceHeight = 60
creatureSize = 10
def resize((width, height)):
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-10.0, livingSpaceWidth * 10.0 + 10.0, livingSpaceHeight * 10.0 + 10.0, -10.0, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def initLivingSpace():
for x in range(livingSpaceWidth):
livingSpace.append([])
for y in range(livingSpaceHeight):
livingSpace[x].append(random.randint(0, 1))
def draw():
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, 3.0)
glColor4f(1.0, 0.0, 0.0, 1.0)
glBegin(GL_QUADS)
for column in range(livingSpaceWidth):
for row in range(livingSpaceHeight):
if isAlive(column, row):
x = column * 10.0
y = row * 10.0
glVertex3f(x, y, 0.0)
glVertex3f(9.0 + x, y, 0.0)
glVertex3f(9.0 + x, 9.0 + y, 0.0)
glVertex3f(x, 9.0 + y, 0.0)
glEnd()
def isAlive(x, y):
return livingSpace[x][y] == 1
def getNeighborCount(x, y):
count = 0
xpn = (x + 1) % livingSpaceWidth
ypn = (y + 1) % livingSpaceHeight
count += isAlive(x , ypn)
count += isAlive(xpn, ypn)
count += isAlive(xpn, y)
count += isAlive(xpn, y - 1)
count += isAlive(x , y - 1)
count += isAlive(x - 1, y - 1)
count += isAlive(x - 1, y)
count += isAlive(x - 1, ypn)
return count
def calculateNextGeneration():
neighborCount = []
for column in range(livingSpaceWidth):
neighborCount.append([])
for row in range(livingSpaceHeight):
neighborCount[column].append(getNeighborCount(column, row))
for column in range(livingSpaceWidth):
for row in range(livingSpaceHeight):
if 2 <= neighborCount[column][row] <= 3:
if neighborCount[column][row] == 3:
# creature gets born
livingSpace[column][row] = 1
else:
# creature dies
livingSpace[column][row] = 0
def main():
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
screenSize = (livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize)
pygame.init()
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
draw()
calculateNextGeneration()
pygame.display.flip()
if __name__ == '__main__': main()
Original aus dem Buch ansehen gameOfLife_v2.py · Python 2
'''
Created on 09.08.2009
@author: Lars Heppert
'''
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *
import random
livingSpace = []
livingSpaceWidth = 100
livingSpaceHeight = 60
creatureSize = 10
def resize((width, height)):
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-10.0, livingSpaceWidth * 10.0 + 10.0, livingSpaceHeight * 10.0 + 10.0, -10.0, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def initLivingSpace():
for x in range(livingSpaceWidth):
livingSpace.append([])
for y in range(livingSpaceHeight):
if random.randint(0, 1) == 1:
livingSpace[x].append(1000)
else:
livingSpace[x].append(0)
def draw():
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, 3.0)
glBegin(GL_QUADS)
for column in range(livingSpaceWidth):
for row in range(livingSpaceHeight):
healthStatus = float(livingSpace[column][row]) / 1000.0
# die Farbe fuer die belebten Felder wird
# nun in Abhaengigkeit vom Wert des Feldes bestimmt
glColor4f(healthStatus, 0.0, 0.0, 1.0)
x = column * 10.0
y = row * 10.0
glVertex3f(x, y, 0.0)
glVertex3f(9.0 + x, y, 0.0)
glVertex3f(9.0 + x, 9.0 + y, 0.0)
glVertex3f(x, 9.0 + y, 0.0)
glEnd()
def isAlive(x, y):
return livingSpace[x][y] == 1000
def getNeighborCount(x, y):
count = 0
xpn = (x + 1) % livingSpaceWidth
ypn = (y + 1) % livingSpaceHeight
count += isAlive(x , ypn)
count += isAlive(xpn, ypn)
count += isAlive(xpn, y)
count += isAlive(xpn, y - 1)
count += isAlive(x , y - 1)
count += isAlive(x - 1, y - 1)
count += isAlive(x - 1, y)
count += isAlive(x - 1, ypn)
return count
def calculateNextGeneration():
neighborCount = []
for column in range(livingSpaceWidth):
neighborCount.append([])
for row in range(livingSpaceHeight):
neighborCount[column].append(getNeighborCount(column, row))
for column in range(livingSpaceWidth):
for row in range(livingSpaceHeight):
if 2 <= neighborCount[column][row] <= 3:
if neighborCount[column][row] == 3:
# creature gets born
livingSpace[column][row] = 1000
else:
# creature dies slowly
livingSpace[column][row] = livingSpace[column][row]/1.1
if livingSpace[column][row] < 200:
livingSpace[column][row] = 0
def main():
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
screenSize = (livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize)
pygame.init()
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
draw()
calculateNextGeneration()
pygame.display.flip()
if __name__ == '__main__': main()