Künstliche Intelligenz anhand eines evolutionären Algorithmus

Für mein Abitur konnte ich freiwillig eine besondere Lernleistung, also ein größeres Projekt erstellen und somit eine mündliche Prüfung ersetzen. Ich habe mich damals entschieden, einen “selbstlernenden” Algorithmus in Java zu implementieren. Jetzt, knapp anderthalb Jahre nachdem ich mit der Implementierung begonnen habe, schreibe ich diesen """Blog"""-Eintrag.

Das hat vor allem den Grund, dass ich ab und zu Fragen zu diesem kleinen Projekt kriege und ich diese nun einfach an diesen Beitrag weiterleiten kann, anstatt kläglich daran zu scheitern, das ganze in kurze Worte fassen zu wollen. Des weiteren möchte ich hier die (weniger guten) Entscheidungen, die ich während der Implementation getroffen habe, reflektieren. Folgend die Dokumentation, die ich damals über das Projekt geschrieben habe. Gefundene Rechtschreibfehler könnt ihr gerne behalten. Oder schreibt mir eine E-Mail.

Direkt zum Fazit?

Einführung

Was ist ein selbstlernender Algorithmus?

Ein selbstlernender Algorithmus ist ein Programmablauf, der eine spezielle Aufgabe ohne Hilfe eines Menschen zu lösen versucht. Dabei ist das Ziel des Algorithmus, eigenständig eine Lösung für eine gegebene Aufgabe zu finden. Es gibt unterschiedliche Arten von Algorithmen, die auf unterschiedlich komplexen Modellen basieren. Dabei ist auffällig, dass viele dieser Modelle die Natur nachahmen. Ein neuronaler Algorithmus ist beispielsweise dem Prinzip unseres Gehirns sehr ähnlich. In dieser Lernleistung wurde ein anderer, allerdings ebenfalls an die Natur angelehnter Algorithmus implementiert, der „genetische Algorithmus“. Dies ist ein evolutionärer Algorithmus, dessen Funktionsweise sich an der Evolution unserer Spezies, der natürlichen Selektion orientiert.

Was ist ein evolutionärer Algorithmus?

Das Prinzip der natürlichen Selektion basiert auf der Weitergabe und Vermischung der elterlichen Gene und den dabei zufällig entstehenden Mutationen. Auch genannt „natürliche Auslese“ bedeutet dies, dass unter schwierigen Lebensbedingungen nur diejenigen die am besten an ihre Umwelt angepasst sind, die Chance haben, sich fortzupflanzen. Daher haben vorteilhafte Gene eine größere Chance, weitergegeben zu werden und sich mit denen eines anderen Elternteils zu vermischen, wodurch ständig neue Kombinationen entstehen, die eventuell eine Verbesserung zur vorherigen Generation darstellt. Daraus resultiert eine kontinuierliche Verbesserung dieser Gene basierend auf den Überlebenschancen der jeweiligen Individuen. Die „Überlebenschancen“ werden in evolutionären Algorithmen generell durch die „Fitness“ ersetzt. Diese wird nach jeder Generation berechnet und gibt an, wie gut sich ein Individuum in seiner gegebenen Aufgabe geschlagen hat. Je höher die Fitness ist, desto höher ist die Chance des Individuums sich fortzupflanzen. Als Aufgabe für den zu entwickelnden Algorithmus fiel die Entscheidung auf einen 2D-Hindernisparcour, den der Algorithmus fehlerfrei auf dem schnellsten Weg lösen soll. Die natürliche Selektion wird durch eine Fitness-Funktion emuliert, welche die Punkte basierend auf ihrer Leistung bewertet. Daraufhin werden Eltern ausgewählt, die dann eine neue Generation erschaffen, welche mit den neuen Anweisungen versucht, die Aufgabe besser zu bewältigen als ihre Vorgänger. Der Mutationsvorgang, die Eigenschaften der Generationen und das Benehmen des Algorithmus sind konfigurierbar.

Funktionsweise und Darstellung des entwickelten Algorithmus

image

Der Aufbau des Spielfelds

Allgemein

Der entworfene Algorithmus soll, wie bereits beschrieben, einen kurzen „Hindernis-Parcour“ durchlaufen. Dieser besteht aus einem Spielfeld, dem Startpunkt, einem Zielfeld und bis zu zwölf Hindernissen. Die Punkte sollen einen möglichst schnellen Weg von ihrer Position zum Ziel finden. Berühren sie die Hindernisse oder den Spielfeldrand, „sterben“ sie.

Planung und Struktur des Algorithmus

Eines der Ziele dieses Projekts war, dass viele Faktoren sowohl des Programms als auch des Algorithmus vollständig konfigurierbar sind. Nahezu alle Werte der Population, der natürlichen Selektion und der Mutation, sowie die Umgebung des Algorithmus (also das Spielfeld) sind frei anpassbar. Alle verfügbaren Einstellungen, deren Effekt auf das Ergebnis und den Ablauf des Algorithmus werden später in dieser Dokumentation noch ausführlich beschrieben. Wenn man sehr viele Einstellungen gegeben hat, sollte man auch die Möglichkeit haben, diese dauerhaft zu speichern. Da diese Daten nicht ständig geändert werden müssen, werden sie in einfache .txt-Dateien geschrieben, jeweils ein Wert pro Zeile.

Die Klasse „Bevoelkerung“

Die Klasse Bevoelkerung speichert eine Liste mit allen Individuen der aktuellen Population und bietet Methoden, um alle Punkte der Bevölkerung auf einmal ansteuern zu können. Sie speichert ebenfalls Attribute des Levels, wie bspw. die Position des Startfeldes, sodass die Punkte zu Beginn an die richtige Stelle gesetzt werden können. Zudem befinden sich hier alle Methoden, die für die natürliche Selektion und die Mutation zuständig sind. Dafür greift sie in vielen Fällen auf die Methoden der darunterliegenden Klasse Punkt zu.

Die Klasse „Punkt“

Diese Klasse ist wohl die wichtigste des Algorithmus. Sie speichert die Attribute der einzelnen Individuen, wie beispielsweise ihre aktuelle Position, ob der Punkt das Ziel erreicht hat oder tot ist, sowie die Fitness der Punkte. Hier befinden sich ebenfalls die Methoden, die für die Darstellung, die Mutation und die „Hitdetektion“ (auch: hitdetection) der Punkte zuständig ist. Letztere überprüft, ob ein Punkt mit einem Hindernis kollidiert.

Die Klasse “Moveset”

Jeder Punkt besitzt ein individuelles Moveset, welches in diesem Algorithmus die Gene darstellt. Die in den Genen enthaltene Information wird von Generation zu Generation weitergegeben. Eine Verbesserung der Gene wird nicht gezielt herbeigeführt, sondern entsteht zufällig: Durch das Vermischen der Gene unterschiedlicher Punkte, durch die zufällige Veränderung eines Gens (Mutation) oder durch beides zusammen. Die angesprochenen Informationen sind „Richtungen“, die vorgeben, wohin sich ein Punkt bewegt. Dafür befindet sich in der Klasse Moveset eine Liste, in dem eine Abfolge von Bewegungen in Form von Punkten auf einem Koordinatensystem gespeichert wird. In die Richtung dieser Koordinaten bewegen sich die Punkte mit einer festen Schrittweite. Die Klasse Moveset bietet Methoden, die es ermöglichen, ein Moveset zu erstellen, dieses zu kopieren und zu mutieren.

Die Hitdetektion

Die Hitdetektion war abseits vom Algorithmus der aufwändigste aber auch wichtigste Teil des Programms. Er ist dafür zuständig, festzustellen, wann ein Punkt mit einem Hindernis oder dem Spielfeldrand kollidiert. Dies war einer der wenigen Teile, in die ich völlig ohne einen Plan oder Vorwissen gegangen bin und auch der Teil, der am öftesten umgestellt wurde.

Koordinatenbasierte Hitdetektion

image

Koordinatenbasierte Hitdetektion

Zuerst habe ich eine koordinatenbasierte Hitdetektion implementiert. Diese vergleicht die Position des Punktes mit den Koordinaten der Eckpunkte eines Vierecks und bestimmt durch eine Bedingung, ob die Position des Punktes innerhalb des Vierecks liegt. Wenn nun die X-Position des Punktes größer als die X-Position von A und kleiner als die von B ist, liegt der Punkt definitiv in in dem Grau markierten Feld der Abbildung. Ergänzt man diese Bedingung nun um die Y-Achse, sieht sie wie folgt aus:

if(pos.x > a.x && pos.x < b.x && pos.y > a.y && pos.y < d.y) {

Die Variable pos stellt dabei die Position des Punktes dar. Trifft diese Bedingung zu, ist der Punkt mit dem Hindernis kollidiert und wird vom Programm auf den Status tot gesetzt. Bei der Implementierung gab es keine erwähnenswerten Probleme. Diese Art der Hitdetektion war für die Zeit der Entwicklung des Algorithmus völlig ausreichend, allerdings erschien die Beschränkung auf gradlinige vier- oder rechteckige Hindernisse relativ schnell sinnlos, da dies die Anzahl der möglichen Spielfelder sehr stark verringert. Aus diesem Grund wurde die Hitdetektion komplett überarbeitet.

Linienbasierte Hitdetektion

Anstatt mit den Koordinaten des gesamten Rechtecks zu arbeiten, wird die Form nun in einzelne Linien getrennt und überprüft, ob der Punkt auf diesen Linien liegt. Diese Methode lässt auch Hindernisse zu, in denen die Linien nicht in einem 90 Grad-Winkel voneinander abgehen. An Hand des oben gegebenen Beispiels würde das bedeuten, dass überprüft wird, ob die Position des Punktes auf einer der Linien zwischen AB, BC, CD und DA liegt. Jedenfalls war das die Idee. In der Praxis hat dies aus dem Grund leider wenig gut funktioniert, dass, wie in der Beschreibung der Moveset-Klasse erwähnt, die Punkte jeweils mit einer festgelegten Schrittweite in eine bestimmte Richtung gehen.

image

Linienbasierte Hitdetektion

Das Problem damit ist, dass ein Punkt also mit dem richtigen Timing “über die Linie springen“ kann, seine Position also nie wirklich auf der Linie liegt, sondern erst kurz davor und nach dem nächsten Schritt kurz dahinter. Behoben habe ich dieses Problem, indem ich die Position des Punkts vor dem Schritt in einer Variablen gespeichert habe und dann überprüft habe, ob die Linie aus der neuen und alten Position sich mit der Linie des Hindernisses schneiden.

Ansonsten gab es bei der Implementierung der Hitdetektion keine weiteren Probleme. Allerdings war es unsicher, wie stark diese Hitdetektion die Performance und die Rechenleistung des Programms beeinflussen würde. Entgegen allen Erwartungen hat dieser Prozess allerdings keinen besonders großen Einfluss auf die benötigte Rechenleistung des Programms. Mehr zu diesem Thema im Themenpunkt Effizienz und Rechenleistung.

image

Graphische Eigenheiten

Allerdings gibt es ein paar kleinere grafische „Unschönheiten“. Das Problem ist, dass die Position des Punktes durch den Mittelpunkt des Punkts bestimmt wird und dass die Hitdetektion einen Punkt erst als „tot“ ansieht, wenn der Mittelpunkt die Linie schneidet. Dies lässt sich auch auf der Abbildung erkennen. Verstärkt wird dieser Effekt durch die schwarzen Ränder, die Hindernisse visuell klar vom Spielfeld abgrenzen sollen. Auch diese zählen nicht zur Position der Hindernisse, können also ebenfalls von den Punkten berührt werden, ohne dass dies die Hitdetektion erkennt. Aus diesem Grund sieht es teilweise so aus, als könnten die Punkte Ecken schneiden, bzw. sterben nicht bereits, wenn sie das Hindernis berühren, sondern erst wenn sie sich ein Stück in dieses hineinbewegen.

Die Implementierung des Algorithmus

Kriterien für die Implementierung des Algorithmus

  • Der Algorithmus hat keinerlei „Wahrnehmung“ von seiner Umgebung und dem Aufbau des Levels, ist also sozusagen blind. Der Weg zum Ziel wird über mehrere Generationen „antrainiert“. Allerdings kann der Algorithmus die Veränderung der Punktzahl über mehrere Generationen beobachten und darauf basierend kleinere Änderungen am Verhalten vornehmen. (Dies verhindert, dass der Algorithmus steckenbleibt.)
  • Ist der Algorithmus einmal gestartet, läuft und trifft dieser alle Entscheidungen eigenständig. Er kann nichtmehr von dem Benutzer des Programms kontrolliert und es können keine Einstellungen mehr geändert werden.

Vorgehen

Als die Klassen Bevoelkerung, Punkt und Moveset soweit fertig waren, wurde mit der Implementierung des eigentlichen Algorithmus begonnen. Mit dem Grundgerüst des Algorithmus fertig, wurden alle Einstellungsmöglichkeiten abgearbeitet, die zuvor gesammelt wurden. Dazu gehört das Erstellen von Variablen für die Einstellung, das Hinzufügen der Variable zum Lese- und Schreibeprozess in die .txt-Datei und das Anlegen von Checkboxen oder Eingabefeldern über die eine Änderung vorgenommen werden kann.

Probleme bei der Implementierung

Während der Implementierung sind mehrere Probleme aufgetreten, die aber in allen Fällen eher eine „Blockade“ meinerseits als ein grundsätzliches Problem mit dem Algorithmus waren. Viele der Probleme sind durch Unachtsamkeiten oder kleine Fehler entstanden, wie bspw. ein fehlerhaftes Konvertieren einer Variablen, wodurch sie auf 0 gesetzt wurde, das rechnen mit einem falschen Wert und das Verdrehen von Übergabewerten bei Methodenaufrufen.

Zum Glück wurden ab und an Sicherungen des Fortschritts gemacht, was es erlaubt hat, einfach noch einmal von vorne anzufangen, als bspw. der erste Entwurf des Mutations-Prozesses unübersichtlich wurde. Hier wurde der Fehler gemacht, dass von Anfang an alle Variablen, die auf unterschiedliche Teile der Bevölkerung angewendet werden müssen, direkt in eingebaut wurden. Dies hat dazu geführt, dass eine riesige Menge an Variablen und Bedingungen vorlag, ohne dass klar war, wie die Mutation überhaupt am besten vorgenommen werden sollte.

Nach einem Neuanfang wurden in allen Teilen des Programms erst die eigentliche Funktion implementiert und diese erst nach erfolgreichem Test um die geplanten Einstellungsmöglichkeiten ergänzt. Außerdem gab es immer wieder Probleme bei der Berechnung der Fitness, was sich auf kleinere Mathe-Probleme zurückführen lässt. Dies hat dazu geführt, dass wohl oder übel Stoff aus der Mittelstufe wiederholt werden musste.

Zudem müssen viele dieser Rechnungen pro Generation tausendfach durchgeführt werden, weshalb auch die Rechenleistung eine gewisse Rolle gespielt hat und oft mehrere Möglichkeiten ausprobiert werden mussten. Teilweise verkürzte das Ändern einer Rechenmethode die Zeit, die zum Generieren einer Generation benötigt wird, um mehrere Sekunden.

Die grafische Oberfläche des Programms

Das Kontrollfenster

Während der Algorithmus läuft, wird in Zusatz zu dem Hauptfenster ein weiteres kleineres Fenster gestartet. Die Aufgabe des Kontrollfensters ist das bereitstellen von Statistiken, die zum Einschätzen des Fortschritts des Algorithmus benötigt werden.

Beispielsweise kann man hier ablesen, wie viele Punkte gestorben sind und wie viele das Ziel erreicht haben, sowie die insgesamte Länge des Movesets und die Position, an der sich die Punkte aktuell befinden. Dies ist wichtig, um herauszufinden, ob das aktuelle Moveset eventuell alles in allem zu kurz für das Level ist.

Zudem kann hier kontrolliert werden, ob man alle Punkte angezeigt bekommen möchte, oder nur den besten. Diese Option macht es einfacher, den Fortschritt der Punkte zu beobachten. Zu beachten ist hier allerdings, dass jeweils der beste Punkt der letzten Generation angezeigt wird, da der Algorithmus den besten Punkt der aktuellen Generation nicht weiß, bevor nicht die Fitness berechnet wurde. Kurz gesagt wird also der beste Punkt der vorherigen Generation angezeigt, während im Hintergrund die neue Generation generiert wird.

Im Unterpunkt „Bester Punkt“ werden Statistiken über den besten Punkt der letzten Generation angezeigt, wie bspw. die Fitness und sobald ein Punkt das Ziel erreicht hat, wie viele Schritte er dafür benötigt hat. Zudem wird hier der aktuelle Rekord für das Level angezeigt, welcher in der txt-Datei des Levels gespeichert wird.

Des Weiteren lässt sich der Algorithmus hier auch pausieren und wieder starten und man kann ins Hauptmenü zurückkehren.

Die grafische Oberfläche des Level-Editors

image

Der Level-Editor ist eine möglichst simpel gehaltene Anwendung, die das Erstellen von Leveln für den Algorithmus möglich machen soll.

  1. Die Größe des Spielfeldes wird durch Ziehen festgelegt.
  2. Das Startfeld und der Startpunkt werden festgelegt. Die Größe des Start- und Zielfeldes können angepasst werden.
  3. Das Zielfeld wird festgelegt.
  4. Es können bis zu 12 Hindernisse und deren Position ausgewählt werden, indem jeweils vier Koordinaten ausgewählt werden. Es gibt außerdem eine Option, die das Löschen von Hindernissen ermöglicht.

Zwischen den Schritten wird jeweils als Bestätigung auf „Weiter“ geklickt. Bevor diese Bestätigung erfolgt ist, kann ein Großteil der Positionen der Elemente nachträglich geändert werden, indem man sie erneut anklickt.

Mein damaliges Fazit:

Effizienz und benötigte Rechenleistung

Der Algorithmus kann auf nahezu jedem Prozessor ausgeführt werden. Allerdings muss die Populationsgröße an die Rechenleistung angepasst werden. Muss der Prozessor zu viele Punkte gleichzeitig anzeigen, sieht die Darstellung nicht mehr flüssig aus. Bei einer Population mit einer Größe von 2500 setzt sich die gesamte benötigte Rechenleistung aus knapp 80% für das Rendern der Punkte und nur aus ~15% durch die Hitdetektion zusammen. Die restlichen 5% werden für das Darstellen des Hauptfensters und des Kontrollfensters verwendet. Die Rechenleistung, die zur Generierung einer neuen Generation benötigt wird, lässt sich schlecht messen, man kann aber aufgrund der geringen benötigten Zeit davon ausgehen, dass dort keine große Rechenleistung mehr eingespart werden kann. Eine höhere Effizienz des Algorithmus kann also vor Allem, durch das Vereinfachen des Darstellungsprozesses der Punkte erzielt werden. Um dies zu erreichen, wurde eine Funktion implementiert, die es ermöglicht, nur den besten Punkt anzuzeigen, was sowohl den Prozessor als auch den Grafikprozessor erheblich entlasten.

image

Worst-Case-Szenario

Probleme des Algorithmus

Worst-Case

Aufgrund der Faktoren, basierend auf welchen der Algorithmus die Fitness der Punkte berechnet, gibt es sogenannte „Worst-Case“-Level, die der Algorithmus nicht schaffen kann. Dies sind grundsätzlich Level, in denen der Algorithmus sich auf seinem Weg zum Ziel, erst von diesem entfernen muss. Auf der Abbildung kann man erkennen, dass die Punkte sich an ihrem Startpunkt bereits sehr nahe am Ziel befinden und daher bereits dort eine sehr große Punktzahl bekommen. Es gibt daher kein Grund für sie, sich vom Ziel zu entfernen und dafür eine geringere Punktzahl zu bekommen, obwohl sie diese Taktik letztendlich zum Ziel führen würde.

Bekannte Probleme

Ein weiteres Problem, welches direkt auf dem Problem des Worst-Case aufbaut, ist dass Punkte, sobald sie sich für einen Weg entschieden haben, sie diesen Weg nur noch schwer ändern können, selbst wenn der gewählte Weg nicht zum Ziel führt.

image

‘Bad’-Case-Szenario

Im gegebenen Beispiel würden die Punkte wahrscheinlich rechts am Hindernis vorbeigehen, da dies der einfachere Weg zu sein scheint. Der Fakt, dass sich die Entfernung zum Ziel verringert, bestätigt dies für die Punkte. Stellt sich der Weg nun als Sackgasse heraus, ist es bereits zu spät für die Punkte, da sich die Gene bereits sehr stark an diesen Weg angepasst haben.

Lösungsansätze

Diese Art von Problemen lassen sich bei einem evolutionären Algorithmus kaum verhindern. Sie entstehen, weil der Algorithmus keine „Sehkraft“ hat und zeigen auch, dass die Evolution nicht immer den bestmöglichen Weg finden kann. Trotzdem lassen sich noch einige weitere Einstellungsmöglichkeiten implementieren, die das Verhalten des Algorithmus verbessern können. Es könnte beispielsweise nicht nur die Entfernung zum Ziel zur Berechnung der Fitness verwendet werden, sondern in bestimmten Fällen durch die Entfernung zum Start ersetzt werden. Es würden also Punkte dafür vergeben werden, wenn sich ein Punkt vom Start entfernt.

Anhang

Schematischer Ablauf des Algorithmus

while(running) {
    if(!b.sindAllePunkteTot()) {
        /* Hier werden die Punkte jeweils geupdated, das bedeutet,
        * dass sie jeweils einen Schritt weiter gesetzt werden.
        * Zudem wird überprüft ob sie durch diesen Schritt mit einem  
        * Hindernis kollidieren oder das Ziel erreicht haben.
        */
        b.updateDiePunkte();

        // Zeigt die Punkte an ihrer aktuellen Position an.
        b.zeigeDiePunkte();
    } else {
        /* Hier wird die Fitness für jeden Punkte berechnet. Diese
        * basiert auf unterschiedlichen Attributen, für die
        * Fitness-Punkte verteilt werden.
        *
        * Die höchste Punktzahl wird dann erreicht, wenn ein Punkt das
        * Ziel mit möglichst wenigen Schritten erreicht.
        * Erreicht ein Punkt das Ziel nicht, bekommt er weniger Punkte.
        * Seine Fitness basiert dann darauf, wie nah er
        * dem Ziel gekommen ist.
        */
        b.berechneFitnessDerPunkte();

        /* In dieser Methode findet die eigentliche Natürliche
        * Selektion statt. Es werden die Punkte mit einer hohen
        * Fitness ausgesucht, basierend auf diesen wird dann
        * eine neue Generation erstellt.
        */
        b.natuerlicheSelektion(punkteAus1, punkteAus2, punkteAus1und2);

        /* In dieser Methode findet die Mutation statt. Das Moveset der
        * vorher generierten Generation wird zufällig mutiert, es werden
        * also mit einer bestimmten Chance einzelne Richtungen geändert.
        * Daraufhin beginnt der Prozess wieder von vorne - es wird
        * überprüft, ob die neue Generation sich gegenüber ihren
        * Vorfahren verbessert haben.
        */
        b.mutation();
    }
} // end of while

Mein heutiges Fazit (Oder: Hinterher ist man immer schlauer)

Rückblickend habe ich die Art des Projekts falsch gewählt. In einem evolutionären Algorithmus ist es gerade das spannende, wenn man viele unterschiedliche Arten von Genen hat und man beobachten kann, wie sich das Individuum verhält, wenn unterschiedliche Gene unterschiedlich stark ausgebildet werden. In meinem Fall war die einzige Art von Genen, mit denen ich arbeiten konnte, die Bewegungsdaten der Punkte. Das hat dazu geführt, dass das ganze relativ eindimensional war und man das Potenzial der “Evolution”, die diese Art von Algorithmus bereits im Namen hat, nicht richtig ausnutzen konnte.

Viel spannender wäre beispielsweise das folgende Szenario gewesen:

  • Die Punkte werden an zufälligen Positionen um das Spielfeld herum gesetzt
  • Im Spielfeld wird zufällig Nahrung verteilt
  • Ein Punkt überlebt nur, wenn er in einer Runde Nahrung aufgesammelt hat
  • Die Menge der verfügbaren Nahrung wird stärker reduziert, je mehr Runden es gibt
  • Jeder Punkte hat unterschiedlich stark ausgebildete Attribute:
    • Gewicht, bzw. Größe: Ein schwerer Punkt kann einen leichten Punkt aus dem Weg schubsen
    • Geschwindigkeit: Schnelle Punkte kommen schneller zu verfügbarer Nahrung
    • Sichtweite: Punkte haben ein unterschiedlich großes Sichtfeld. Punkte mit einem kleinen Sichtfeld benötigen aber weniger Zeit um zu reagieren, wenn Nahrung in ihr Sichtfeld kommt.
    • Energie: Alle Punkte haben die selbe Anzahl an Energie. Schnelle Punkte verbrauchen mehr Energie/Schritt, langsame weniger. Schwere Punkte verbrauchen mehr Energie, leichte weniger.
    • Punkte die besonder groß sind, können kleine Punkte aufnehmen und benötigen in dieser Runde keine Nahrung mehr
    • Außerdem könnte man sich noch weitere Attribute ausdenken, zum Beispiel könnte ein Punkt mit einer bestimmten Gen-Ausprägung länger als eine Runde ohne Nahrung überleben können.

In diesem Szenario hätte man beobachten können, welche der Attribute für die Punkte einen Vorteil darstellen und welche Attribute nach einiger Zeit aussterben. So könnte es zum Beispiel sein, dass nach einigen Runden nur noch schnelle, leichte Punkte mit kleinem Sichtfeld übrig sind, weil sie schneller zur Nahrung kommen und so Schnelligkeit einen klaren Evolutionsvorteil darstellt. Es wäre aber auch denkbar, dass schwere Punkte mit großem Sichtfeld einen größeren Vorteil haben, weil sie andere aufnehmen und Nahrung auch in größerer Entfernung wahrnehmen können.

Denkbar wäre auch, dass ein Hybrid aus den verfügbaren Attributen den anderen überlegen ist. So ist ein evolutionärer Algorithmus viel spannender als einfach nur den von mir implementierten “Pathfinding”-Algorithmus.

Sollte ich nocheinmal die Möglichkeit haben, ein solches Projekt im Rahmen der Universität abzuschließen oder sollte ich irgendwann mal zu viel Zeit haben, wäre das auf jeden Fall eine spannende Möglichkeit.

Wieder hoch?