Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

ws1718:strukturierte_dokumentation

Dies ist eine alte Version des Dokuments!


Strukturierte Dokumentation

Einführung

Im November 2018 kamen wir in unserem Projektlabor Mathesis zur Ideenfindung neuer Projekte. Anhand von Gruppen-brain-storming wurden schon einzelne Ideen in den Raum geworfen. Hierbei kam die gesamte Gruppe ins Gespräch über Vorstellungen und Interessen. Auf diesem Wege fanden wir uns als Projektgruppe zusammen.
Schnell kamen Diskusionen über das Thema unseres Projektes auf. Dabei war uns neben dem programmiertechnischen Aspekt, auch ein interessantes Thema wichtig. Interesse sahen wir besonders in einem uns unbekannten Themenfeld, welches jedoch aktuelle Relevanz hat. Inspiration suchten wir dabei vor allem im Internet, speziell auf Youtube. Dort stießen wir auf das Video Mar I/O. In diesem Video wird gezeigt, wie Mario, gesteuert von einem Programm, mit jedem Durchlauf mehr lernt die Welt zu erledigen.
So kamen wir schon sehr bald auf den Gedanken in unserem Projekt eine künstlichen Intelligenz zu erzeugen. Woraufhin wir nun nach spezifischen Ideen suchten, um dieses Konzept umzusetzen. Wichtig dabei war es ein Projekt zu finden, welches nicht zu einfach ist, jedoch auch nicht unsere Programmierfähigkeiten übersteigt. Erneut gab uns Youtube die finale Idee. Der Evolutionssimulator weckte unser Interesse so stark, dass die Ideen für ein eigenes Programm nur so sprudelten.
Die programmiertechnische Herausforderung einer künstlichen Intelligenz in Verbindung mit dem wissenschaftlichen Aspekt der Evolution schien perfekt. Die Entscheidung fiel, wir programmmieren einen Evolutionssimulator.

Die Grundidee unseres Evolutionssimulators ist es Kreaturen zu erschaffen, die alle in Spezien mit unterschiedlichen Eigenschaften unterteilt sind. Die Spezien untereinander zeichnen sich durch unterschiedliche Schrittlänge und Richtungsänderung aus. Wir lassen unsere Spezien in einem Umfeld voller Hindernisse aus. Dabei kollidieren und sterben sie sowohl an den Hindernissen, als auch an den Rändern des Umfeldes. Alle Kreaturen werden am Ende einer Generation durch ein Rating bewertet. An Hand dieses Ratings können wir nun erkennen, welche Kreaturen sich besonders gut verhalten haben. Die besonders guten Kreaturen vererben darauf hin ihre guten Eigenschaften mit einer gewissen Möglichekit zur Mutation an die jeweils nächste Generation.

An dieser Stelle wollen wir den Bezug zu den bekannten Evolutionstheorien ziehen.
Größtenteils ist unsere Simulation an die Theorie Lamarcks angelehnt. Er ging davon aus, dass Organismen Eigenschaften, an ihre Nachkommen weiter geben können. Diese Eigenschaften wären in unserer Simulation der Weg einer Kreatur. Desto besser diese Eigenschaften sind bzw. der Weg einer Kreatur bewertet wird, umso mehr setzen sich diese in den nächsten Generationen durch.
Einen zusätzlichen Blickwinkel liefert uns Darwins Evolutionstheorie. Er nimmt dabei verschiedene Aspekte an. Die für unsere Simulationn wichtigen Aspekte seiner Theorie sind die Variation, Selektion und Vererbung. Nach Darwin ist eine Population nie gleich, sondern unterscheidet sich in mehreren Merkmalen. So wie sich unsere Spezien in den speziellen Mermalen unterscheidet. Hinzu kommt, dass Organismen, die zufällig besser angepasst sind an den Lebensraum, ihre Gene verhäuft in die nächste Generation vererben. Parallel dazu lässt sich in unserer Simualtion das Rating beschreiben. Die Kreaturen, die zufällig einen besseren Weg wählten, setzen sich in der nächsten Generation besser durch.
Zu guter Letzt haben unsere Kreaturen immer eine gewisse Rate zur Mutation. Solange diese Mutation sich positiv auf den Weg unserer Kreaturen auswirkt, setzen sich die mutierten Gene/ Eigenschaften besser in den kommenden Generationen durch.

Zusammenfassend können wir unseren Evolutionssimulator als Mischung beider Evolutionstheorien betrachten. Um dies besser nach zu vollziehen, werfen wir zunächst einen Blick auf den Code.
Unser Programm besteht aus zwei Codes. Im Vordergrund steht der Hauptcode CreatureR. Er ist für die Erstellung, Bewegung, Bewertung und Vererbung der Kreaturen zuständig. Der Visualisierungscode pygame greift hierbei auf den Hauptcode zu. Mit ihm ist es uns möglich die Simulation grafisch dar zustellen.


Hauptcode


Visualisierungscode

Visualisierungscode

Um eine Evolution erfolgreich darstellen zu können, brauchen wir, neben dem Hauptcode, auch einen idealen Visualisierungscode.

Turtle

Für analytische Zwecke nutzten wir durchgehend Turtle. Das Programm zeigte uns präzise das Verhalten unserer virtuell erschafften Kreaturen an. Besonders hilfreich wurde das Paket bei der Programmierung der Vererbung. Sehr gut war bei Turtle erkennbar, ob eine Tochtergeneration tatsächlich auf die Werte der Mutter zugreift oder nicht. Als Hauptvisualisierungsprogramm fiel Turtle jedoch raus. Zum einen bietet Turtle keinen grafischen Spielraum und zum anderen ist es nicht möglich sich mehrere Kreaturen gleichzeitug zeichnen zu lassen. Somit ist es sichtlich ungeignet für eine detailierte Darstellung unserer Ideen.

Pygame

PyGame bot uns im Gegensatz zu Turtle wesentlich umfangreichere Visualisierungsmöglichkeiten für unser Projekt. Einer der wichtigsten Vorteile dieses Pakets war dabei, dass wir damit in der Lage dazu waren, mehrere Kreaturen gleichzeitig auf dem Bildschirm erscheinen zu lassen. Mit Turtle konnte man maximal eine Kreatur beobachten und so zwar den Hauptcode verbessern, aber nicht dessen volles Potential graphisch darstellen. Wie der Name schon sagt ist PyGame an sich für Videospiele mit Python entwickelt worden und ist leider nicht besonders gut dokumentiert. Die beste Dokumentation, die wir gefunden hatten waren die Tutorials von KidsCanCode auf YouTube.
Das grundsätzliche Konzept eines jeden Videospiels, das man mit PyGame programmiert, ist die sogenannte Game Loop. Diese sorgt dafür, dass das Spiel angezeigt wird und sich je nach bestimmten Eingaben etwas auf dem Bildschirm verändert. Auf dem Folgenden Bild erkennt man die Bestandteile einer Game Loops. In diese unterteilt sich auch der Skeleton eines PyGame-Projekts.

Quelle: https://youtu.be/VO8rTszcW4s

Der Process Input ist alles, was extern vom Code selbst eingegeben wird. Dazu gehören z.B. Interaktionen mit der Maus oder den Tasten auf dem Keyboard. Auch so etwas wie ein „x“ am oberen Fensterrand zum Schließen des Programms muss in diesen Bereich des Codes eingebaut werden.

In der Update Section werden in jedem Frame die neuen Daten verarbeitet, die sich seit dem letzten Frame verändert haben, in unserem Fall also jegliche Fortbewegungen der Kreaturen.

Die Render Section kann man auch als Drawing Section bezeichnen. Sie lässt die Daten, die sich während des Updates verändert haben, auf dem Bildschirm ausgeben.

Die Uhr am Ende der Zeichnung soll symbolisieren, dass die Game Loop sich in jedem Frame wiederholt, wobei dies innerhalb von einer Sekunde z.B. 24 Mal (24 fps = Anzahl der Bilder für ein flüssiges Bild) oder auch 60 Mal (Bildrate in modernen Videospielen) geschehen kann. Je nach dem wie gut der Prozessor des verwendeten Endgerätes ist, kann das Programm dann auch bei einer zu hohen Framerate ruckeln oder hängen.

Nach dieser etwas längeren Einführung nun zum eigentlichen Code.

PyGame Skeleton

Das Grundgerüst unseres PyGame-Codes haben wir aus dem Skeleton Code von KidsCanCode übernommen. Da dieser für jedes PyGame Projekt so gut wie gleich ist, werden wir ihn an dieser Stelle nicht näher erklären, sondern auf das zugehörige Tutorial verweisen.
Auf dieser Basis konnten wir dann die nötigen Klassen, Funktionen und Counter einführen, um die Hindernisse, Kreaturen und Statistiken illustrieren zu können.

Klassen

Hindernisse

Für alle Hindernisse, die auf unserer Karte zu sehen sind (namentlich die Regentropfen, die Wolke und die Plattformen) haben wir Klassen angelegt. Von der Struktur her sind die Klassen sehr ähnlich, weswegen hier nur eine exemplarisch erklärt wird.

Das ist die Klasse für unsere Regentropfen:

class DROP(pygame.sprite.Sprite):
	"""Regentropfen"""
 
	def __init__(self, x, y):
		pygame.sprite.Sprite.__init__(self)
 
		self.x = x
		self.y = y
 
		self.image = pygame.image.load(os.path.join(img_folder, "drop.png")).convert()
		self.image.set_colorkey(BLACK)
		self.image = pygame.transform.scale(self.image, (30,50))
		self.mask = pygame.mask.from_surface(self.image)
		self.rect = self.image.get_rect()
		self.rect.center = (self.x, self.y)

Der Klasse wird ein PyGame Sprite übergeben. Da alle Objekte in PyGame als Sprites bezeichnet werden, ist dies an dieser Stelle obligatorisch.
Genauso muss in jeder Klasse eine __init__-Funktion enthalten sein, der relevante Variablen (in diesem Fall z.B. die x- und y-Koordniaten) übergeben werden, die für die Vorgänge in der Klasse benötigt werden.
Als erste Zeile in dieser Funktion muss zudem die __init__-Funktion selbst initialisiert werden. Darauf folgt das Einbauen und Positionieren der Regentropfen-Bilder.
Dazu müssen zunächst die übergebenen x- und y-Koordinaten mit ihrer eigenen Selbstreferenz gleichgesetzt werden.
Mit den drei Zeilen, die mit „self.image“ beginnen wird zuerst erreicht, dass das Bild namens „drop.png“ aus einem zusätzlichen Ordner namens „img“ geladen wird. Der .convert()-Befehl muss an das Ende des Ausdrucks gesetzt werden, sonst entsteht eventuell eine falsche Darstellung des eigentlichen Bildes.
Der set_colorkey-Befehl entfernt jegliche Stellen im Bild mit der vermerkten Farbe. Alle selbstgezeichneten Bilder wurden freigestellt und mit einem schwarzen Hintergrund ausgestattet, sodass durch diesen Befehl das Bild ohne schwarzen Hintergrund auf dem Bildschirm zu sehen ist.
Der transform.scale()-Befehl, der in der darauffolgenden Zeile verwenden wird, gibt uns die Möglichkeit ohne zusätzliche Bildbearbeitung, direkt in PyGame die Größe des Bildes bequem anzupassen. Dabei steht die erste Zahl in den Klammern für die Länge und die zweite für die Breite.
Bevor die nächste Zeile verstanden werden kann, muss der self.rect-Befehl erklärt werden. Hier wird nämlich erstmal ein Rechteck (rect) um das Regentropfen-Bild mit dem Schwarzen Hintergrund erstellt. Im Weiteren wird dieses Rechteck mit dem .center-Befehl in die Mitte des Bildes gelegt. Das ist bei jedem PyGame-Sprite erforderlich, damit Pygame das Bild auch als Objekt wahrnehmen und nachverfolgen kann. Zwar können Bilder freigestellt werden, jedoch gelten sie im PyGame-Algorithmus nur als Rechtecke mit speziellen Eigenschaften. An dieser Stelle kommt die ausgelassene Zeile und der self.mask-Befehl ins Spiel.
Wir möchten bei unseren Hindernissen und auch später bei unseren Kreaturen nicht, dass (selbst wenn man es wegen den freigestellten Bildern so nicht sieht) nur die Rechtecke um die Objekte herum mit einander kollidieren, sondern die Umrisse der Bilder, wie sie durch die freigestellten Bilder sichtbar sind. Dafür wird eine Maske (mask) erstellt. Diese wird bei der Kollision weiterverwendet werden. Diese Bilderreihe soll besser veranschaulichen, was diese Stelle im Code bewirkt: Quelle: https://youtu.be/Dspz3kaTKUg

Kreaturen

class CREA(pygame.sprite.Sprite):
 
	"""erstellt die kreaturen"""
 
	def __init__(self,spec):
		pygame.sprite.Sprite.__init__(self)
		self.c1 = cr.Creature(species=spec)
		self.c1.appendWay()
		self.image = all_images[spec]
 
		self.spec = spec
		self.rect = self.image.get_rect()
		self.rect.center = (0,0)
 
		self.block_index = 0
 
	def update(self):
		if self.block_index < len(self.c1.briefing):
			block = self.c1.briefing[self.block_index]
 
			self.c1.nextStep(block)
			self.rect.center = (self.c1.stats["pos"][0]+100 , self.c1.stats["pos"][1]+360)
 
			self.block_index += 1
 
			if self.c1.stats["step_counter"] < moveCap and not self.c1.stats["dead"]:
				self.c1.appendWay()
 
		else:
			self.c1.stats["dead"] = True
 
		if (self.c1.stats["pos"][0]+100) < 0 or (self.c1.stats["pos"][0]+100) > 1000:
			self.c1.stats["dead"] = True
 
 
		if (self.c1.stats["pos"][1]+360) < 0 or (self.c1.stats["pos"][1]+360) > 720:
			self.c1.stats["dead"] = True

Wie bei jeder anderen Klasse auch in pygame, wird der CREA Klasse zunächst ein Pygame Sprite übergeben. In der __init__-Funktion wird diese obligatorisch selbst initialisiert in der ersten Zeile.
Zudem wird der __init__-Funktion ein Variable spec zugewiesen. Diese Variable beinhaltet die Information, welche Spezie realisiert werden soll. Dazu greifen wir auf die Klasse Creature des Hauptcodes zu und erhalten alle nötigen Eigenschaften der Kreatur.
Es folgt die Zeile „self.c1.appendWay()“. Diese Funktion der Creature Klasse des Hauptcodes ist dafür zuständig neue Schritte für die Kreatur zu generieren. Beim Erstellen der Kreatur werden hier direkt eine gewisse Anzahl Schritte erstellt. Die eigentliche Bewegung findet erst in der update-Funktion statt.
Anschließend ordnen wir jeder Kreatur ein Spezies abhängiges Bild zu ( „self.image = all_images[spec]“). Somit erreichen wir zusätzlich einen visuellen Unterschied der Kreaturen gemäß ihrer Art.
Die Zeilen, die mit „self.rect“ beginnen, umgeben das Bild mit einem rectangle und positionieren dieses. Das Prinzip ist das selbe, wie in den Klassen der Hindernisse.
Für die nächste Funktion der Klasse ist die letzte Zeile der __init__-Funktion wichtig. Sie liefert uns eine Variable, die später einen Index- Counter bildet. Um diese genauer nach zu vollziehen werfen wir einen Blick auf die update-Funktion der CREA-Klasse.
Die update-Funktion der CREA-Klasse ist vor allem wichtig für die Bewegung unserer erzeugten Kreaturen. Hier kommt der „self.block_index“ -Counter zum Einsatz. Zunächst stellen wir hier eine Bedingung auf. Sie bewirkt, dass die Bewegung der Kreatur nur solange von statten geht, wie Schritte (gespeichert im briefing) vorhanden sind. Ansonsten stirbt die Kreatur.
Auf Grund der Strukturierung des briefings arbeiten wir hier mit zwei Indizier. Das briefing besteht aus einer Liste von Blocks. Ein Block beschreibt einen Schritt. Die Blocks enthalten Informationen über die Richtung und Schrittlänge.
Zunächst speichern wir in der Variable „block“ den aktuellen Schritt ab, der jetzt ausgeführt werden soll. Damit beim nächsten Durchlauf der nächste Schritt ausgeführt wird, erweitern wir den „self.block_index“-Counter um 1.
Dann greifen wir auf eine weitere Funktion der Creature-Klasse des Hauptcodes zu. „nextStep(block)“ ist dafür zuständig die Kreatur einen Schritt ausführen zu lassen. Alles was sie dazu braucht sind die Informationen über den Schritt. Diese übergeben wir anhand der „block“ – Variable, die eben diese Informationen enthält.
Um die Kreatur auch visuell zu bewegen, müssen die Koordinaten des rects bewegt werden. Hier setzen wir dieses einfach auf die neuen x- und y-Koordinaten der Kreatur. Diese Informationen erhalten wir ebenfalls aus dem Hauptcode. („self.rect.center = self.c1.stats[“pos“][0]+100, self.c1.stats[“pos“][1]+360“)
Zusätzlich kommt hier der „stepCounter“ zur Sprache. Die Liste der Schritte(briefing) wird immer schrittweise verlängert. Wenn der „stepCountergeringer als die movecap und die Kreatur nicht als tot gemeldet wurde (zum Beispiel durch Kollision), wird erneut die Funktion „appendWay()'“ der Creature-Klasse des Hauptcodes verwendet. Diese fügt immer neue Schritte zum briefing hinzu, solange die Bedingungen erfüllt sind.
Nach dieser Bedingung finden wir noch zwei weitere Bedingungen in der update-Funktion. Diese überprüfen mit jedem Durchlauf die Koordinaten der Kreaturen. Wenn diese über den Bildschirmrand hinaus sind, werden die Kreaturen auf dead gesetzt und sterben.

weiter im Code...

Nachdem wir unsere Klassen definiert haben, wird es wichtig unsere Sprites zu gruppieren und auf zurufen. Alle Objekte, die wir erstellen, ob Hindernisse oder Kreaturen, ordnen wir zunächst zu der Gruppe „all_sprites = pygame.sprite.Group()“ zu. Diese ist wichtig für jeden Update- Durchlauf. Somit können wir direkt alle Objekte gleichzeitig aktualisieren.
Zusätzlich ordnen wir jedes Objekt entweder in die Hinderniss-Gruppe („obstacles = pygame.sprite.Group()“) oder in die Kreaturen-Gruppe („ colliC = pygame.sprite.Group()“) ein. Diese Unterteilung wird später wichtig für die Kollision sein.
Nachdem wir diese Gruppen defimierten, erstellen wir eine Liste alles Kreaturen. In dieser Liste werden alle Informationen der einzelnen Kreaturen gespeichert. Anhand des Ratings und der „.sort()“-Funktion hilft diese uns eine neue Generation zu erstellen. Später dazu mehr.

Es folgen zwei in sich greifende Schleifen zur Erstellung der Kreaturen. Die erste Schleife mit dem Parameter i, wird solange ausgeführt, wie Kreaturen erstellt werden sollen. Die zweite Schleife mit dem Parameter n, geht rückwerts durch die Anzahl an Spezien. Mit diesen beiden Schleifen wird erzielt, dass soviel Kreaturen erstellt werden, wie es angegeben ist. Dabei wird zusätzlich auf die gerechte Verteilung der Spezienanzahl geachtet. Nach Erstellung der Kreatur, wird diese sowohl in die allgemeine Sprite- Gruppe, als auch in die Kreaturen- Gruppe, als auch in die Liste „creature“ geordnet.

Als nächstes werden einige Funktionen wichtig…

Funktionen...

...zum Erstellen der restlichen Objekte

Wolke, Plattformen und Tropfen werden alle gleich erstellt. Jedes Objekt wird durch Aufrufen der Klasse und Angabe der Koordinaten erstellt. Daraufhin wird es zunächst der allgemeinen Sprite_Gruppe und der Hinderniss-Gruppe zu geordnet.

...für die Kreaturen

==bubbly(): Die bubbly()-Funktion ist zuständig für die Kollision. Das Prinzip ist eine simple hit-box-Abfrage. Mit Hilfe des „spritecollide()“- Befehls wird für jedes Objekt in der Kreaturen-Klasse eine Kollisionsabfrage mit jedem Objekt der Hinderniss-Klasse gemacht. Falls die Abfrage positiv sein sollte, wird die Kreatur auf dead gesetzt.

newGen(n)

Nun kommen wir zu einer äußerst wichtigen Funktion der Simulation. Als erstes fügen wir an dieser Stelle zwei Counter ein, die in der Visualisierung mit abgebildet werden.
Der erste Counter („gencnt“) zählt die Generationen mit und wird dementsprechend bei jedem Abruf der Funktion um 1 erweitert.
Der zweite Counter („ratingcnt“) wird bei jedem Abruf der Funktion genullt. Erst nachdem alle Kreaturen erstellt werden, wird dieser mit dem Rating aller Kreaturen erweitert.
Beide Counter werden im laufendem Programm am rechten Bildrand angezeigt. Somit kann man mit den fortschreitenden Generationen, die Entwicklung unserer Evolution erkennen.

Um nun eine neue Generation zu erstellen, nutzen wir zunächst die angekündigte „.sort()“-Funktion. Durch sortieren der creatures-Liste, wird uns eine absteigende Reihenfolge der Ratings der Kreaturen ausgegeben.
Für die neu erstellte Liste nutzen wir eine erste Schleife mit dem „enumerate“-Befehl. Mit ihm gelingt es uns sowohl durch den Index i, als auch durch das Element c von „creatures“ getilt durch n zu gehen. n beschreibt hier den Selektionsdruck und legt somit fest, wie viele Kreaturen vererben dürfen.
In der ersten Schleife speichern wir unter der Variable „spec“ die jeweilige Spezie jedes Elementes der Liste.
Es folgt eine zweite Schleife, die durch eine von n-anhängige Listegeht. Dabei wird für jedes n-tel eine neue Kreatur abhängig der vererbten Eigenschaften erstellt ( „cr.procreation(c.c1)“). Anschließend wird mit der „appendWay()“- Funktion für die neue Kreatur ein neuer Weg erstellt (abnhängig von den vererbten Eigenschaften). Zu guter Letzt wird jeder neu erstellten Kratur ihre alte Spezie durch die Variable „spec“ zugeordnet und ein dazu passendes Bild.

Somit haben wir eine neue Generation, die auf die Eigenschaften der Mutter zugreift.

def newGen(n):
	"""neue Generation"""
	global gencnt
	global ratingcnt
	gencnt+=1
	ratingcnt = 0
 
	for c in creatures:
		c.block_index = 0
		ratingcnt += c.c1.career["score"]	
 
	creatures.sort(key=lambda w: w.c1.rating(), reverse=True)
 
	for i,c in enumerate(creatures[0:len(creatures)/n]):
		spec = c.spec
		for j in range(n)[::-1]:
			creatures[(len(creatures)/n)*j+i].c1 = cr.procreation(c.c1)	#hier
			creatures[(len(creatures)/n)*j+i].c1.appendWay()
			creatures[(len(creatures)/n)*j+i].spec = spec
			creatures[(len(creatures)/n)*j+i].image = all_images[spec]
stats()

Zu dieser Funktion gibt es nicht viel zu sagen. Die Funktion ist dafür zuständig die Kreaturen jeder Spezie zu zählen. Die Anzahl wird mit jedem Durchlauf genullt. Mit einer Doppelschleife gehen wir durch die creatures-Liste und einer erzeugten Liste abhängig der Spezienzahl. Wenn die allgemeine Spezienzahl mit der Spezienzahl der Kreatur über einstimmt, wird der jeweilige Counter erhöht. Auf die Werte dieser Funktion greifen wir später beim Erstellen der Balken zurück.

def stats():
	"""	Anzahl der Spezien	"""
	global speccnt
	speccnt = [0 for i in range(amountSp)]
 
	for c in creatures:
		for n in range(amountSp):
			if c.spec == n:
				speccnt[n]+=1

...für die Ausgabe auf dem Bildschirm

draw_text

Die draw_text()-Funktion ist dazu da, Text und sich verändernde Werte auf dem Bildschirm auszugeben.

def draw_text(surf,text,size,x,y):
	"""	ermöglicht das Schreiben in den screen	"""
	font= pygame.font.Font(font_name, size)
	text_surface = font.render(text, True, BLACK)
	text_rect = text_surface.get_rect()
	text_rect.topleft=(x,y)
	surf.blit(text_surface,text_rect)

Bei „font=“ wird festgelegt, welche Schriftart in welcher Größe für die Ausgabe verwendet werden wird. Dazu wird der Funktion „font_name“ und „size“ übergeben. „font_name“ wurde vorher oben im Visualisierungscode unter #initialisieren so definiert:

font_name= pygame.font.match_font('brush script')

Mit dem font.match_font-Befehl wird eine auf dem verwendeten Endgerät gespeicherte Schriftart von PyGame ausgewählt und imitiert. Da die Schriftart, für die wir uns entscheiden haben nicht auf jedem Rechner vorhanden ist, kann es sein, dass das Schriftbild an anderen Computern anders aussieht.
Im nächsten Schritt unter „text_surface=„kann man der Verwendung von Kantenglättung zustimmen und eine Textfarbe auswählen. Am besten erkennt man an dem Bild rechts, was mit Kantenglättung eigentlich gemeint ist:
Mit „True“ haben wir uns für „Anti-aliased“ entschieden.
Im Weiteren wird um den Text ein Rechteck erstellt und mit dem .topleft-Befehl in der oberen linken Ecke ausgerichtet. Mit dem surf.blit-Befehl wird der Text vor alle anderen Objekte gezeichnet.
Quelle: http://www.snowbound.com/sites/snowbound.com/files/images/antialias.gif

draw_bar

Die draw_bar()-Funktion haben wir für die bunten Überlebensbalken am rechten Bildrand eingebaut.

def draw_bar(surf, x, y, pct, clr):
	"""	zeicnet Balken für die Skala	"""
	if pct < 0:
		pct = 0
	BAR_LENGTH = 100
	BAR_HEIGHT = 20
	fill = (pct/100.*BAR_LENGTH)
	outline_rect = pygame.Rect(x,y,BAR_LENGTH, BAR_HEIGHT)
	fill_rect = pygame.Rect(x,y,fill,BAR_HEIGHT)
	pygame.draw.rect(surf,clr,fill_rect)
	pygame.draw.rect(surf, BLACK, outline_rect,2) 

Die if-Bedingung in der Funktion bewirkt, dass der Balken niemals negative Werte darstellen kann, indem solche Werte abgefangen und auf 0 gesetzt werden.
Durch “BAR_LENGTH“ und „BAR_HEIGHT“ deklarieren und initialisieren wir Variablen für die Länge und Breite des Balkens.
In der „fill=“-Zeile wird eine Variable festgelegt, die die Länge des bunten Balkens proportional zu dem jeweiligen pct-Wert verändert. Dass die Veränderung auch dynamisch auf dem Bildschirm mitzuverfolgen ist, wird mit Hilfe von fill_rect erreicht. Durch outline_rect= legen wir um den beweglichen bunten Balken einen statischen Umriss. Die pygame.draw.rect()-Funktionen fungieren als Zusammenfassung aller vorher festgelegten Eigenschaften eines Balkens, was die Ausführung dieser in der draw()-Funktion erleichtert. Da die zwei gleichnamigen Funktionen unterschiedlich viele Übergabevariablen benötigen, dürfen sie denselben Namen tragen.

draw

Die „draw()“-Funktion wurde dazu erstellt, die zuvor beschriebenen Funktionen an einer Stelle gesammelt ausführen zu können, sodass Änderungen leicht eingebunden werden können.

def draw():
	#->screen
	screen.fill(BLACK)
	screen.blit(background, (0,0)) 
	pygame.draw.rect(screen, ROSE, [1000, 0, 280, 720])
	pygame.draw.rect(screen, BLACK, [1000, 0, 280, 720], 2)
	pygame.draw.rect(screen, BLACK, [0, 0, 1000, 720], 2)
 
	#->sprites
	all_sprites.draw(screen)
	#->Text
	draw_text(screen,("Start"), 25, 65,320)
	draw_text(screen,("Ziel"), 25, 870,320)
	draw_text(screen,("Generation: " + str(gencnt)), 30, 1050,10)
	draw_text(screen,("Sieger: " + str(finpergencnt)), 30, 1050,50)
	draw_text(screen,("Rating: " + str(int(ratingcnt/amountEnt))), 30, 1050, 90)
	draw_text(screen,("Einhorn:"), 18, 1050,150)
	draw_text(screen,("Pinguin:" ), 18, 1050,200)
	draw_text(screen,("Schmetterling:"), 18, 1050,250)
 
	#->Balken
	draw_bar(screen, 1050,170, speccnt[0], WHITE)
	draw_bar(screen, 1050,220, speccnt[1], BLACK)
	draw_bar(screen, 1050,270, speccnt[2], YELLOW)

Unter #->screen haben wir die pygame.draw.rect()-Funktion verwendet, um am rechten Rand des Fensters ein violettes Rechteck zu erstellen, welches die Fläche bildet, auf der die Werte und die Balken dargestellt werden. Des Weiteren wird hier auch ein schwarzer Umriss um ebendieses Rechteck und den gesamten Hintergrund gezogen.
Die Funktion all_sprites.draw(screen) ermöglicht die Darstellung aller Sprites auf dem Bildschirm.
Unter #->Balken und #->Text werden die die draw_bar- und draw_text-Funktionen aufgeführt. In der hier verzeichneten Version unseres Codes werden nur 3/10 Spezies abgebildet. Die Syntax ist jedoch auch für die weiteren Spezies analog. Zu diesen Abschnitten ist noch zu sagen, dass Ausdrücke der Form str(gencnt) den Zweck erfüllen, die Zahl, die sich hinter der eingeklammerten Variable verbirgt, in einen String umzuwandeln, da die draw_text-Funktion nur Text, also Strings, ausgeben kann. Die verschiedenen Counter-Variablen werden an einer anderen Stelle näher erläutert.

Game loop

Damit wir am Ende auch alles darstellen können, kommen wir jetzt zum eigentlichen „Game loop“.
Hier greifen wir auf das bekannte Skeleton von Pygame zu, welches wir schon am anfangs verlinkten.
Wir setzen also running auf „True“, rufen alle Sprites (außer die Kreaturen) mit der „defSprites()“ auf und gehen dann in die obligatorsiche while-Schleife.

"""	Game loop	"""
running = True
defSprites()
while running:
	clock.tick(FPS)
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			running = False
		if event.type == pygame.KEYDOWN:
			if event.key == pygame.K_ESCAPE:
				pygame.quit()

Wir rufen zunächst die Kollisionsfunktion „bubbly()“ auf. Anschließend überprüfen wir mit einer for-Schleife durch die creatures-list, ob alle Kreaturen tot sind. Sollte dies der Fall sein, erstellen wir eine neue Generation mit der „newGen()“-Funktion. Zusätzlich rufen wir an dieser Stelle „stats()“ ab, um die Spezienanzahl der neuen Generation zu zählen.

	bubbly()
 
 
	for c in creatures:
		if not c.c1.stats["dead"]:	
			allDead  = False
			break
		allDead = True
 
	if allDead:
		newGen(selection)
		stats()

Nach diesem Schritt kommen wir zu unserer Update-section. Hier werden zunächst alle Sprites mit dem Befehl „all_sprites.update()“ aktualisiert. Der „finpergencnt“-Counter zählt alle Kreaturen, die das Ziel erreicht haben. Dieser wird nach jeder Generation wieder genullt.

	#Update
	all_sprites.update()
 
	finpergencnt = 0
 
	for c in creatures:
		if c.c1.stats["success"]:
			finpergencnt+=1

Abschließend wird in der draw-section unsere „draw()“-Funktion aufgerufen. Hiermit zeichnen wir alles in den screen und aktualisieren diesen mit der „pygame.display.flip()“-Funktion.

	#zeichnen
	draw()
 
	pygame.display.flip()

Verlauf der Projektarbeit

Die Anfänge unseres Projektes sind sehr gut unserem Konzeptblatt zu entnehmen. Schaut man sich dieses an, wird schnell klar, dass es zu Anfang viele substanzielle Fragen für unser Projekt zu klären gab. Dabei musste von dem Fortbewegungskonzept bis hin zur Definition des Sterbens alles diskutiert werden.
Ursprünglich haben wir zwecks Strukturierung unserer Arbeitsphasen einen Zeitplan 1) erstellt, in den wir jeden Labor-Termin vermerkt und uns vorgenommen haben, jede Woche Aufgaben für die nächste Woche festzulegen. Wie das so oft mit frühen Ideen geschieht, haben wir nach dem erstem Mal keine Eintragungen in den Zeitplan gemacht und die „Hausaufgaben“ zur nächsten Labor-Stunde mündlich abgemacht.
Als unser Projekt noch in den Kinderschuhen steckte, einigten wir uns zudem nicht auf eine klare Arbeitsteilung und entschieden uns statt dessen alle Schritte bis zur Vollendung des Projekt vorerst gemeinsam auszuführen, da wir z.T. vor dem Programmierkurs zu Beginn des Semesters noch nie programmiert hatten und erst eine Gruppendynamik einkehren musste.
Die Arbeit an beiden Codes erforderte viel Einarbeitung. So mussten wir uns zu Beginn noch ausführlich über Klassen und Python Pakete wie NumPi, Turtle und PyGame informieren, ohne die unser Code in seiner jetzigen Form nicht existieren könnte.
Während wir die Eigenschaften der Kreaturen, die Rating-Kriterien, die Fortbewegung (Schatzsuche-Prinzip) vergleichweise früh im Projekt festgelegt und zu einem großen Teil so beibehalten haben, gab es vor allem bei der Vererbung und dem damit verbundenen Rating viele Stolpersteine, die überwunden werden mussten. Der Beginn der Programmierung des Vererbungsmechanismus markierte auch unsere Aufteilung in zwei Untergruppen, da an diesem Punkt die Zeit langsam knapp wurde und das Herzstück des gesamten Programms noch nicht programmiert wurde. So übernahm Lorenz die Programmierung am Hauptcode und Lena und Daphna den Visualisierungscode und die künstlerische Gestaltung. Dies war auch der einzige Punkt im gesamten Projekt, an dem die Motivation der Teilnehmer gleichermaßen niedrig lag, da so große und bedeutende Abschnitte einer Projektarbeit ihre entmutigende Wirkung haben.
Diese Demotivation löste sich jedoch mit den nächsten Fortschritten im Hauptcode. Ansonsten herrschte stets eine sehr angenehme und motivierte Arbeitsatmosphäre geprägt von gegenseitiger Unterstützung und Verständnis.
Eine andere mit der Vererbung verbundene Fragestellung war die Wahl des Selektionssmechanismus. Dieses Thema bot viel Diskussionspotential, da Python eine große Menge an Paketen für Neuronale Netze bietet und auch das Bayes'sche Netz in Frage kam. Angesichts er Tatsache, dass wir unter Zeitdruck standen und unser eigens entworfener Selektionscode vielversprechende Ergebnisse lieferte, entschieden wir uns, diesen auszubauen und kein bekanntes Konzept zu adaptieren.
Im Visualisierungscode gab es ebenfalls viele Entscheidungen zu treffen. So entschieden wir uns für ein bei einer solchen Art von Projekten eher untypischen Art-Style und beschlossen, die künstlerische Gestaltung selbst in die Hand zu nehmen, um die Übersichtlichkeit und Benutzerfreundlichkeit unseres Programms zu erhöhen. Dabei wurden Mind-Maps 2) für die Arten der Kreaturen, deren Farbgebung und das Design des Umfeldes erstellt. Des Weiteren mussten Konzepte zur Ausgabe unserer verschiedenen Counter-Variablen gefunden werden, woraus die Idee mit der Balkendarstellung entstanden ist.
Die Aufteilung in Untergruppen hatte ebenfalls zur Folge, dass der Hauptcode lange Zeit nicht auf den Visualisierungscode abgestimmt war. Dieses Problem lösten wir im Laufe der Blocktermine, indem wir uns wieder als ganze Gruppe wie zu Beginn zusammenfanden. Die Zusammenarbeit außerhalb der Laborzeiten hat der Datenaustausch über die TubCloud erheblich erleichtert, wobei bei einer größeren Anzahl an Teilnehmern die Menge an neuen Code-Versionen wahrscheinlich schnell unübersichtlich geworden wäre.
Zum Ende der drei Blocktermine hatten wir eine vollkommen illustrierte Map und einen funktionierenden Selektionsmechanismus, die wir in der Präsentation 3) unseren Kursmitgliedern vorstellen konnten. In der Vorlesungsfreien Zeit nahmen wir uns die Kritikpunkte unserer Dozenten vor und haben nun einen komplett überarbeiteten und besser strukturierten Hauptcode mit einem kompatiblen bug-freien Visualisierungscode fertiggestellt.
Abschließend lässt sich zum Verlauf der Projektarbeit sagen, dass, obwohl die Projektarbeit nicht zu 100% strukturiert verlief, das gute Arbeitsklima und die hohe Motivation aller Beteiligten zu einer effizienten Arbeitsweise und zu Letzt zu einem Endprodukt geführt hat, auf das wir alle stolz sind.


Fazit/Ausblick

Wie man im Projektverlauf erkennen kann, entwickelte sich unser Projekt und dessen Strukturen ständig. Immer wieder verwarfen wir Ideen und bauten neue mit in unser Konzept ein. So wurden auch Entscheidungen getroffen, die vorher nicht absehbar waren, oder endlose Diskussionen geführt, die zur Stagnation führten.
Genau das macht dieses Projekt aus. Eine anhaltende Entwicklung und Verbesserung des Codes, die zu mehr spannenden Fragen führten. Daraus zogen wir selbst ständig neue Motivation mehr Arbeit in dieses Projekt zu stecken. Demnach haben wir heute auch einen Code auf den wir stolz sein können. Unser Ziel eine Evolution zu simulieren und diese visuell darzustellen, haben wir erreicht.
Dabei haben auch unsere persönlichen Ziele, wie die Weiterbildung im Programmieren und eine Auseinandersetzung mit neuen, aktuellen Themen, Erfüllung gefunden. Das Projekt bat eine gute Basis gemeinsam an einem Ziel zu arbeiten und sich dabei auf den unterschiedlichsten Ebenen weiter zu bilden.
Vor allem aus diesem Grund finden wir immer wieder neue Motivation zur Weiterentwicklung unseres Programms. Momentan liegt diese vor allem im Visualisierungscode. Die größte Baustelle ist dabei den Code effizienter und flexibler zu machen. Dazu gehört eine ähnliche Umstrukturierung und Parametisierung, wie im Hauptcode. Aber auch Kleinigkeiten, wie eine flexible Fenstergröße gehören zu den Baustellen des Projekts.
Unter Evolution versteht man eine allmähliche Weiterentwicklung der Merkmale einer Population. Man kann unser Projekt nun als solche ansehen. Die erste Generation hat nun ihr Ende erreicht und somit seid nun auch ihr herzlich eingeladen an diesem Projekt mit zu wirken. Wir vererben euch hier alle nötigen Codes und Programme. Macht dieses Projekt zu eurem und lasst eurer Kreativität freien Lauf. Wir hoffen auf eure Inspiration, die zu einer aufregenden Weiterentwicklung des Programms führen.
In diesem Sinne verabschieden wir uns als Gruppe des Evolutionssimulators und wünschen euch viel Erfolg!

ws1718/strukturierte_dokumentation.1522971840.txt.gz · Zuletzt geändert: 2018/04/06 01:44 von lenarost