Das Programm besteht aus zwei Dateien:
In der ersten sind das Game of life und seine Regeln implementiert (sie sollte gameoflife.py heißen) und die zweite greift darauf zu und erstellt eine Animation mit der man interagieren kann.
gameoflife.py:
import random import numpy as np import matplotlib.pyplot as plt import time class GameOfLife(object): """ Diese Klasse beschreibt einen Zustand eines Game of Lifes und stellt Funktionen dazu bereit. Attribute: zustand: der aktuelle Zustand des Game of Life groesse: die Seitenlänge des Spielfeldes tem: die Tripleeinheitsmatrix, die zur Berechnung benötigt wird """ def __init__(self, zustand): """ Erzeugt ein neues GameOfLife-Objekt. Parameter: zustand: der initiale Zustand """ self.zustand = zustand self.groesse = zustand.shape[0] self.generiereTripleeinheitsmatrix() def generiereTripleeinheitsmatrix(self): self.tem = np.zeros((self.groesse, self.groesse)) for i in range(0, self.groesse): for j in range(0, self.groesse): if i == j: self.tem[(i,j)] = 1 else: self.tem[(i,j)] = 0 for i in range(0, self.groesse): for j in range(0, self.groesse): if i == (j + 1): self.tem[(i,j)] = 1 for i in range(0, self.groesse): for j in range(0, self.groesse): if j == (i + 1): self.tem[(i,j)] = 1 self.tem[0, self.tem.shape[1] - 1] = 1 self.tem[self.tem.shape[1] - 1, 0] = 1 def berechneAnzahlNachbarn(self): # Zum Aufsummieren der Nachbarn multipliziert man die vorher mit Tripleeinheitsmatrix(z,s) erstellte Tridiagonalmatrix von beiden Seiten # an die Matrix, in der die Zustaende der zellen gespeichert sind (einmal um die horizontalen und einmal um die vertikalen Partner zu addieren) # als gleichung sieht das ungefaehr so aus:(tem steht hier fuer die tridiagonalmatrix) # anzahlnachbarn = tem*Zustand*tem # mit diesen zwei Matrixmultiplikationen kann man sehr recheneffektiv die 9 nachbarn(zelle selbst mitgezaehlt) # aller Zellen gleichzeitig aufsummieren und dabei haben wir auch noch direkt etwas ueber lineare Algebra gelernt! z = np.dot(self.tem, self.zustand) k = np.dot(z, self.tem) anzahlNachbarn = k - self.zustand return anzahlNachbarn def schritt(self): """ Führt einen Berechnungsschritt aus und gibt den neuen Zustand zurück. """ anzahlNachbarn = self.berechneAnzahlNachbarn() # berechne neuen Zustand # Ursprungszelle Tod + genau 3 lebende Nachbarn = lebend # Ursprungszelle Lebend + genau 1 lebenden Nachbarn = tot # Ursprungszelle Lebend + 2 o. 3 Nachbarn = lebend # Ursprungszelle lebend + 4 oder mehr Nachbarn = tot for i in range(0, self.groesse): for j in range(0, self.groesse): if self.zustand[(i,j)] == 1 and (anzahlNachbarn[(i,j)] in range(2) or anzahlNachbarn[(i,j)] in range(4,9)): self.zustand[(i,j)] = 0 elif self.zustand[(i,j)] == 0 and anzahlNachbarn[(i,j)] == 3: self.zustand[(i,j)] = 1 return self.zustand
Die Animationsdatei (anzeige.py):
from gameoflife import GameOfLife import numpy as np from matplotlib import pyplot as plt from matplotlib import animation import matplotlib.pyplot as plt from PIL import Image from math import floor, ceil class Anzeige(object): """ Diese Klasse animiert ein GameOfLife-Objekt mithilfe der matplotlib. Attribute: gol: das GameOfLife-Objekt, das animiert wird canvas: der matplotlib-Canvas, auf den gezeichnet wird im: die aktuell angezeigte Bitmap zellAlter: speichert für jede Zelle, für wie viele Generationen sie schon lebendig ist animationLaeuft: gibt an, ob die Animation läuft (also nicht pausiert ist) benutzerZeichnet: gibt an, ob der Benutzer gerade auf den Canvas zeichnet anim: das matplotlib-Animationsobjekt """ # unterschiedliche colourmaps (weil ein buntes Leben, lebenswerter ist) # diese werden zufaellig aufgerufen colormaps = ['Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r', 'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap', 'CMRmap_r', 'Dark2', 'Dark2_r', 'GnBu', 'GnBu_r', 'Greens', 'Greens_r', 'Greys', 'Greys_r', 'OrRd', 'OrRd_r', 'Oranges', 'Oranges_r', 'PRGn', 'PRGn_r', 'Paired', 'Paired_r', 'Pastel1', 'Pastel1_r', 'Pastel2', 'Pastel2_r', 'PiYG', 'PiYG_r', 'PuBu', 'PuBuGn', 'PuBuGn_r', 'PuBu_r', 'PuOr', 'PuOr_r', 'PuRd', 'PuRd_r', 'Purples', 'Purples_r', 'RdBu', 'RdBu_r', 'RdGy', 'RdGy_r', 'RdPu', 'RdPu_r', 'RdYlBu', 'RdYlBu_r', 'RdYlGn', 'RdYlGn_r', 'Reds', 'Reds_r', 'Set1', 'Set1_r', 'Set2', 'Set2_r', 'Set3', 'Set3_r', 'Spectral', 'Spectral_r', 'Wistia', 'Wistia_r', 'YlGn', 'YlGnBu', 'YlGnBu_r', 'YlGn_r', 'YlOrBr', 'YlOrBr_r', 'YlOrRd', 'YlOrRd_r', 'afmhot', 'afmhot_r', 'autumn', 'autumn_r', 'binary', 'binary_r', 'bone', 'bone_r', 'brg', 'brg_r', 'bwr', 'bwr_r', 'cividis', 'cividis_r', 'cool', 'cool_r', 'coolwarm', 'coolwarm_r', 'copper', 'copper_r', 'cubehelix', 'cubehelix_r', 'flag', 'flag_r', 'gist_earth', 'gist_earth_r', 'gist_gray', 'gist_gray_r', 'gist_heat', 'gist_heat_r', 'gist_ncar', 'gist_ncar_r', 'gist_rainbow', 'gist_rainbow_r', 'gist_stern', 'gist_stern_r', 'gist_yarg', 'gist_yarg_r', 'gnuplot', 'gnuplot2', 'gnuplot2_r', 'gnuplot_r', 'gray', 'gray_r', 'hot', 'hot_r', 'hsv', 'hsv_r', 'inferno', 'inferno_r', 'jet', 'jet_r', 'magma', 'magma_r', 'nipy_spectral', 'nipy_spectral_r', 'ocean', 'ocean_r', 'pink', 'pink_r', 'plasma', 'plasma_r', 'prism', 'prism_r', 'rainbow', 'rainbow_r', 'seismic', 'seismic_r', 'spring', 'spring_r', 'summer', 'summer_r', 'tab10', 'tab10_r', 'tab20', 'tab20_r', 'tab20b', 'tab20b_r', 'tab20c', 'tab20c_r', 'terrain', 'terrain_r', 'twilight', 'twilight_r', 'twilight_shifted', 'twilight_shifted_r', 'viridis', 'viridis_r', 'winter', 'winter_r'] def __init__(self, gol): """ Erzeugt ein neues Anzeige-Objekt und beginnt die Animation. Parameter: gol: das GamoOfLife-Objekt """ self.gol = gol fig = plt.figure() self.canvas = fig.canvas plt.axis('off') #so wird bei jedem ausfuehren eine zufaellige colormap gewaehlt um ein angenehmeres, vielseitiges Langzeiterlebnis zu kreieren self.im = plt.imshow(self.gol.zustand, animated=True, cmap=plt.get_cmap(Anzeige.colormaps[int(np.random.random()*len(Anzeige.colormaps))])) self.zellAlter = np.zeros(self.gol.zustand.shape) self.zeichne(self.gol.zustand) #Verbinden, der interaktionsevents mit dem figure, in dem die animation laeuft self.canvas.mpl_connect('key_press_event', self.onPress) self.canvas.mpl_connect('button_press_event', self.onClick) self.canvas.mpl_connect('button_release_event', self.onClickEnd) self.canvas.mpl_connect('motion_notify_event', self.onMove) self.animationLaeuft = True self.benutzerZeichnet = False #die tatsaechliche Ausfuehrung: self.anim = animation.FuncAnimation(fig, self.animationsschritt, interval=200) plt.show() def animationsschritt(self, e): ''' Führt einen Animationsschritt durch. ''' self.gol.schritt() self.zeichne(self.gol.zustand) return self.im, def zeichne(self, zustand): ''' Zeichnet den angegebenen Zustand auf den matplotlib-Canvas. ''' # enthaelt Werte ueber die 'Lebensdauer' einer Zelle self.zellAlter = self.zellAlter * zustand + zustand # lebende Zellen aendern ihre Farbe, je laenger sie leben self.im.set_array(np.ones(self.zellAlter.shape) - np.power(0.5, self.zellAlter)) self.canvas.draw() #die vier folgenden Funktionen sind zum Interagieren mit dem Geschehen def onPress(self, event): # zum Pausieren mit der Leertaste if event.key == ' ': if self.animationLaeuft: self.anim.event_source.stop() self.animationLaeuft = False else: self.anim.event_source.start() self.animationLaeuft = True def onClick(self, event): # mit dem Cursor kann man den Wert einer Zelle per Klick aendern von lebend zu tot, sowie andersherum if event.button == 1 and event.xdata != None and event.ydata != None: self.benutzerZeichnet = True self.anim.event_source.stop() self.zustandNeu = np.copy(self.gol.zustand) pos_x = int(np.round(event.xdata)) pos_y = int(np.round(event.ydata)) self.zustandNeu[pos_y, pos_x] = 1 - self.gol.zustand[pos_y, pos_x] self.zellAlter = np.zeros(self.zustandNeu.shape) self.zeichne(self.zustandNeu) def onMove(self, event): # mit dem Cursor kann man den Wert mehrerer Zellen per Klick aendern, indem man die Maustaste gedrueckt haelt # die Animation wird hierfuer pausiert if event.button == 1 and self.benutzerZeichnet and event.xdata != None and event.ydata != None: pos_x = int(np.round(event.xdata)) pos_y = int(np.round(event.ydata)) self.zustandNeu[pos_y, pos_x] = 1 - self.gol.zustand[pos_y, pos_x] self.zellAlter = np.zeros(self.zustandNeu.shape) self.zeichne(self.zustandNeu) def onClickEnd(self, event): # beim loslassen der Maustaste werden die geänderten Werte an die Animation gegeben und diese wird fortgesetzt if event.button == 1 and self.benutzerZeichnet: self.gol.zustand = self.zustandNeu self.zellAlter = np.zeros(self.zustandNeu.shape) self.zeichne(self.zustandNeu) self.benutzerZeichnet = False if self.animationLaeuft: self.anim.event_source.start() ###### Programmeinstieg ####### def main(): # Erzeuge ein GameOfLife-Objekt anhand der Benutzereingaben # die 5 Konfigurationsmodi sind: # 1.zufaellig: hier wird jede Zelle zufaellig einen wert 1 oder 0 bekommen # 2.alternierend: hier wird das Feld in abwechselnd 1- und 0-groesse eingeteilt, mit einem kleinen Fehler in der Mitte # 3.Datei: hier kann direkt eine Konfiguration eingelesen werden. z.B. um eine gleiterkanone oder Aehnliches einzulesen # 4.Bild: eine Bilddatei kann eingelesen und in eine annaehernd erkennbare anfangskonfiguration umgewandelt werden (da der kontrast zu schwarzweiss und die groesse geaendert wird) # 5.Leer: hier wird das Feld mit Nullen gefuellt modus = int(input('Gib den Modus 1 (Zufall), 2 (alternierend), 3 (Datei) oder 4 (Bild) ein: ')) if modus == 1 or modus == 2 or modus == 5: groesse = int(input('Gib die Anzahl der Zeilen/Spalten ein: ')) if modus == 1: # Zufall zustand = np.round(np.random.random((groesse, groesse))).astype(int) elif modus == 2: # Alternierend zustand = np.zeros((groesse, groesse)) for i in range(0, groesse): for j in range (0, groesse): if j%2==0: zustand[(i,j)]=0 else: zustand[(i,j)]=1 if zustand[(int(groesse/2), int(groesse/2))] == 0: zustand[(int(groesse/2), int(groesse/2))] = 1 zustand[(int((groesse/2)+1), int(groesse/2))] = 1 else: zustand[(int(groesse/2), int(groesse/2))] = 0 zustand[(int((groesse/2)+1), int(groesse/2))] = 0 elif modus == 3: # Datei lesen dateiName = input('Was willste oeffnen? ') datei = open(dateiName) dateiAlsListe = [] for line in datei: dateiAlsListe.append([int(c) for c in list(line.rstrip())]) dateiAlsMatrix = np.array(dateiAlsListe) zustand = machQuadrat(dateiAlsMatrix) elif modus == 4: # Bild lesen dateiname = input('Gib den Bildpfad ein: ') bild = Image.open(dateiname) bild = bild.convert('1') bild.thumbnail((500,500)) pixel = np.array(bild) zustand = machQuadrat(pixel) # erzeuge GameOfLife-Objekt gol = GameOfLife(zustand) # initialisiere Anzeige Anzeige(gol) def machQuadrat(pixel): """ Macht das angegebene zweidimensionale Numpy-Array quadratisch, indem es die Ränder mit Nullen auffüllt. """ breite = pixel.shape[0] hoehe = pixel.shape[1] size = max(breite, hoehe) links = floor((size - breite) / 2) rechts = ceil((size - breite) / 2) oben = floor((size - hoehe) / 2) unten = ceil((size - hoehe) / 2) pad_width = ((links, rechts), (oben, unten)) pixel = np.pad(pixel, pad_width, mode='constant', constant_values=0) return pixel main()