Kapitel 2 · Game of Life

Conways Game of Life in Python

Ein Spiel ohne Spieler: Auf einem Raster leben Zellen, und vier einfache Regeln entscheiden, wer in der nächsten Generation lebt. Daraus entstehen wandernde Gleiter, pulsierende Muster und ganze Kolonien. Du kannst mit der Maus oder dem Finger neues Leben säen.

Aus dem Buch · Kapitel 2 Einsteiger SimulationGrafik & Animation Python 3 · läuft im Browser
Bereit · Python startet beim ersten Klick
# Game of Life – Kapitel 2 »Game of Life«, Version mit langsamem Sterben
# aus »Coding for Fun mit Python«, portiert auf Python 3 und c4f
import random
from c4f import Screen, MOUSEDOWN, MOUSEMOVE, KEYDOWN

livingSpace       = []
livingSpaceWidth  = 100
livingSpaceHeight = 60
creatureSize      = 10


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 clearLivingSpace():
    for x in range(livingSpaceWidth):
        for y in range(livingSpaceHeight):
            livingSpace[x][y] = 0


def draw():
    screen.clear()
    for column in range(livingSpaceWidth):
        for row in range(livingSpaceHeight):
            healthStatus = livingSpace[column][row] / 1000.0
            # die Farbe der belebten Felder hängt vom Wert des Feldes ab
            if healthStatus > 0:
                x = column * creatureSize
                y = row * creatureSize
                color = (int(255 * healthStatus), 0, 0)
                screen.rect(x, y, creatureSize - 1, creatureSize - 1, color)


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:
                    # eine Kreatur wird geboren
                    livingSpace[column][row] = 1000
            else:
                # die Kreatur stirbt langsam
                livingSpace[column][row] = livingSpace[column][row] / 1.1

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


def handleEvent(event):
    """Neu im Browser: Zellen per Maus/Finger setzen, Tasten steuern."""
    global running
    if event.type == MOUSEDOWN or (event.type == MOUSEMOVE and screen.mouse_down):
        column = int(event.x // creatureSize)
        row = int(event.y // creatureSize)
        if 0 <= column < livingSpaceWidth and 0 <= row < livingSpaceHeight:
            livingSpace[column][row] = 1000
    elif event.type == KEYDOWN:
        if event.key == "SPACE":
            running = not running
        elif event.key == "r":
            livingSpace.clear()
            initLivingSpace()
        elif event.key == "c":
            clearLivingSpace()


async def main():
    global screen, running
    screen = Screen(livingSpaceWidth * creatureSize, livingSpaceHeight * creatureSize,
                    background="black", title="Game of Life")
    screen.controls([("SPACE", "Pause"), ("r", "Neu würfeln"), ("c", "Leeren")])
    initLivingSpace()
    running = True

    while True:
        for event in screen.events():
            handleEvent(event)
        draw()
        if running:
            calculateNextGeneration()
        await screen.frame(15)  # statt pygame.display.flip(): anzeigen und warten


await main()
Game of Life: rote, teils verblassende Zellen auf schwarzem Raster

Klicken oder Wischen setzt neue Zellen. Leertaste pausiert, »r« würfelt neu, »c« leert das Feld.

Konsole

    

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.

  1. 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
  2. 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
  3. 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:
  4. 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))
  5. 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.1 rechnet 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()