Modulare Ant Bots mit Speicher


Eines der Projekte, von deren Implementierung ich lange getrÀumt hatte, waren modulare Task-Bots mit Speicher. Das ultimative Ziel des Projekts war es, eine Welt mit Kreaturen zu schaffen, die in der Lage sind, unabhÀngig und gemeinsam zu handeln.

FrĂŒher habe ich Weltgeneratoren programmiert, deshalb wollte ich die Welt mit einfachen Bots bevölkern, die mithilfe von KI ihr Verhalten und ihre Interaktionen bestimmen. Dank des Einflusses der Akteure auf die Welt war es somit möglich, ihre Details zu erweitern.

Ich habe bereits das grundlegende Javascript-Task-Pipeline-System implementiert (weil es mein Leben vereinfacht hat), aber ich wollte etwas zuverlĂ€ssigeres und skalierbareres, also habe ich dieses Projekt in C ++ geschrieben. Der Wettbewerb um die Implementierung des Verfahrensgartens in der Subreddit / r / Verfahrensgeneration hat mich dazu gefĂŒhrt (daher das entsprechende Thema).

In meinem System besteht die Simulation aus drei Komponenten: der Welt, der Bevölkerung und einer Reihe von Aktionen, die sie verbinden. Daher musste ich drei Modelle erstellen, die ich in diesem Artikel diskutieren werde.

Um die Schwierigkeit zu erhöhen, wollte ich, dass die Schauspieler Informationen ĂŒber frĂŒhere Erfahrungen mit der Welt behalten und das Wissen ĂŒber diese Interaktionen in zukĂŒnftigen Aktionen nutzen.

Bei der Erstellung eines Weltmodells habe ich einen einfachen Pfad gewÀhlt und Perlin-Rauschen verwendet, um ihn auf der WasseroberflÀche zu platzieren. Alle anderen Objekte auf der Welt wurden absolut zufÀllig lokalisiert.

FĂŒr das Populationsmodell (und sein „GedĂ€chtnis“) habe ich einfach eine Klasse mit mehreren Merkmalen und Koordinaten erstellt. Dies sollte eine Simulation mit niedriger Auflösung sein. Speicher ist eine Warteschlange, Bots werden umgesehen, speichern Informationen ĂŒber ihre Umgebung, schreiben in die Warteschlange und verwalten diese Warteschlange als Interpretation ihres Speichers.

Um diese beiden Aktionssysteme miteinander zu verbinden, wollte ich ein Framework primitiver Aufgaben in einem hierarchischen System von Aufgabenwarteschlangen erstellen, damit einzelne EntitÀten komplexes Verhalten in der Welt implementieren können.


Beispielkarte. Wasser nahm völlig ungewollt die Form eines Flusses an. Alle anderen Elemente sind zufĂ€llig angeordnet, einschließlich des AmeisenhĂŒgels, der in diesem Samen zu weit zum Rand verschoben ist (aber der Fluss sieht wunderschön aus).

Ich entschied, dass ein Haufen Ameisen im Gras, die BlÀtter sammeln, ein gutes Testmodell sein wird, das die ZuverlÀssigkeit der Implementierung der Grundfunktionen (und des gesamten Task-Queue-Systems) garantiert und Speicherlecks verhindert (es gab viele).

Ich möchte die Struktur von Aufgabensystemen und Speicher genauer beschreiben und auch zeigen, wie KomplexitĂ€t aus (meistens) primitiven Grundfunktionen erzeugt wurde. Ich möchte auch einige lustige „GedĂ€chtnislecks von Ameisen“ zeigen, die auftreten können, wenn die Ameisen auf der Suche nach Gras im Kreis herumlaufen oder still stehen und das Programm verlangsamen.

Allgemeine Struktur


Ich habe diese Simulation in C ++ geschrieben und SDL2 zum Rendern verwendet (ich habe bereits zuvor eine kleine PrĂ€sentationsklasse fĂŒr SLD2 geschrieben). Ich habe auch die A * -Implementierung (leicht modifiziert) verwendet, die ich auf github gefunden habe, weil meine Implementierung hoffnungslos langsam war und ich nicht verstehen konnte, warum.

Eine Karte ist nur ein 100 × 100-Raster mit zwei Ebenen - einer Bodenebene (zur Suche nach Pfaden) und einer FĂŒllschicht (zur VervollstĂ€ndigung von Interaktions- und Suchpfaden). Die Weltklasse ĂŒbernimmt auch verschiedene kosmetische Funktionen wie das Wachstum von Gras und Vegetation. Ich spreche jetzt darĂŒber, weil dies die einzigen Teile sind, die im Artikel nicht beschrieben werden.

Die Bevölkerung


Bots waren in einer Klasse mit Eigenschaften, die eine einzelne Kreatur beschreiben. Einige von ihnen waren kosmetisch, andere beeinflussten die AusfĂŒhrung von Aktionen (zum Beispiel die FĂ€higkeit zu fliegen, die Sichtweite, was es isst und was die Kreatur tragen kann).

Am wichtigsten waren hier die Hilfswerte, die das Verhalten bestimmen. NÀmlich: ein Vektor, der ihren aktuellen Pfad A * enthÀlt, damit er nicht in jedem Taktzyklus gezÀhlt werden muss (dies spart Rechenzeit und ermöglicht es Ihnen, mehr Bots zu simulieren), und eine Speicherwarteschlange, die die Interpretation ihrer Umgebung durch die Kreaturen definiert.

Speicherwarteschlange


Eine Speicherwarteschlange ist eine einfache Warteschlange, die eine Reihe von Speicherobjekten enthĂ€lt, deren GrĂ¶ĂŸe durch eine Bot-Eigenschaft begrenzt ist. Jedes Mal, wenn neue Erinnerungen hinzugefĂŒgt wurden, wurden sie vorangebracht und alles, was ĂŒber die Grenzen hinausging, wurde abgeschnitten. Dank dessen könnten einige Erinnerungen „frischer“ sein als andere.

Wenn der Bot Informationen aus dem Speicher abrufen wollte, erstellte er ein Speicherobjekt (Anfrage) und verglich es mit dem, was sich im Speicher befand. Dann gab die RĂŒckruffunktion einen Speichervektor zurĂŒck, der einem oder allen in der Abfrage angegebenen Kriterien entspricht.

class Memory{ public: //Recall Score int recallScore = 1; //Memory Queryable? Array bool queryable[4] = {false, false, false, false}; //Memory Attributes std::string object; std::string task; Point location; bool reachable; 

Erinnerungen bestehen aus einem einfachen Objekt, das mehrere Eigenschaften enthĂ€lt. Diese Speichereigenschaften werden als "miteinander verbunden" betrachtet. Jeder Speicher erhĂ€lt außerdem einen "RecallScore" -Wert, der jedes Mal wiederholt wird, wenn die Erinnerungsfunktion an die Speicher erinnert. Jedes Mal, wenn sich der Bot an die Speicher erinnert, fĂŒhrt er nacheinander eine Sortierung in einem Durchgang durch, beginnend von hinten, wobei die Reihenfolge der Speicher geĂ€ndert wird, wenn der RĂŒckrufwert eines Ă€lteren Speichers höher ist als der eines neuen Speichers. Aus diesem Grund sind einige Speicher möglicherweise „wichtiger“ (bei großen SpeichergrĂ¶ĂŸen) und werden lĂ€nger in der Warteschlange gespeichert. Im Laufe der Zeit werden sie durch neue ersetzt.

Speicherwarteschlangen


Ich habe dieser Klasse auch mehrere ĂŒberladene Operatoren hinzugefĂŒgt, damit direkte Vergleiche zwischen der Speicherwarteschlange und der Abfrage durchgefĂŒhrt werden können, wobei "beliebige" oder "alle" Eigenschaften verglichen werden, sodass beim Überschreiben des Speichers nur die angegebenen Eigenschaften ĂŒberschrieben werden. Aus diesem Grund kann der Speicher des Objekts einem bestimmten Ort zugeordnet werden. Wenn wir diesen Ort jedoch erneut betrachten und das Objekt nicht vorhanden ist, können wir den Speicher aktualisieren, indem wir ihn mit dem Speicher ĂŒberschreiben, der eine neue FĂŒllkachel enthĂ€lt, wobei die Abfrage verwendet wird, die diesem Ort entspricht .

 void Bot::updateMemory(Memory &query, bool all, Memory &memory){ //Loop through all existing Memories //"memories" queue is a member of Bot for(unsigned int i = 0; i < memories.size(); i++){ //If all matches are required and we have all matches if(all && (memories[i] == query)){ //We have a memory that needs to be updated memories[i] = memory; continue; } //If not all matches are required and any query elements are contained else if(!all && (memories[i] || query)){ //When overwriting, only overwrite specified quantities memories[i] = memory; continue; } } } 

Bei der Erstellung des Codes fĂŒr dieses System habe ich viel gelernt.

Aufgabensystem


Die Art der Spielschleife oder des Renderns ist, dass die gleichen Funktionen in jedem Takt wiederholt werden. Ich wollte jedoch nicht zyklisches Verhalten in meinen Bots implementieren.

In diesem Abschnitt werde ich zwei Ansichten zur Struktur des Task-Systems erlÀutern, um diesem Effekt entgegenzuwirken.

Bottom-up-Struktur


Ich beschloss, von unten nach oben zu gehen und eine Reihe von „primitiven Aktionen“ zu erstellen, die Bots ausfĂŒhren sollten. Jede dieser Aktionen dauert nur einen Schlag. Mit einer guten Bibliothek primitiver Funktionen können wir sie zu komplexen Aktionen kombinieren, die aus mehreren primitiven Funktionen bestehen.

Beispiele fĂŒr solche primitiven Handlungen:

 //Primitives bool wait(Garden &garden, Population &population, int (&arguments)[10]); bool look(Garden &garden, Population &population, int (&arguments)[10]); bool step(Garden &garden, Population &population, int (&arguments)[10]); bool swap(Garden &garden, Population &population, int (&arguments)[10]); bool store(Garden &garden, Population &population, int (&arguments)[10]); bool consume(Garden &garden, Population &population, int (&arguments)[10]); bool move(Garden &garden, Population &population, int (&arguments)[10]); //Continue with secondaries here... 

Beachten Sie, dass diese Aktionen Verweise sowohl auf die Welt als auch auf die Bevölkerung enthalten, sodass Sie sie Àndern können.

  • Warten bewirkt, dass die Kreatur in dieser Schleife nichts tut.
  • Look analysiert die Umgebung und stellt neue Erinnerungen in die Warteschlange.
  • Swap nimmt einen Gegenstand in die Hand der Kreatur und ersetzt ihn durch einen auf dem Boden liegenden.
  • Verbrauchen zerstört den Gegenstand in der Hand der Kreatur.
  • Schritt nimmt den aktuell berechneten Pfad zum Ziel und fĂŒhrt einen Schritt aus (mit einer Reihe von FehlerprĂŒfungen).
  • 
 usw.

Alle Aufgabenfunktionen sind Mitglieder meiner Aufgabenklasse. Nach strengen Tests haben sie ihre ZuverlÀssigkeit und FÀhigkeit bewiesen, sich zu komplexeren Aufgaben zu kombinieren.

 //Secondaries bool walk(Garden &garden, Population &population, int (&arguments)[10]); bool idle(Garden &garden, Population &population, int (&arguments)[10]); bool search(Garden &garden, Population &population, int (&arguments)[10]); bool forage(Garden &garden, Population &population, int (&arguments)[10]); bool take(Garden &garden, Population &population, int (&arguments)[10]); //Species Masters bool Ant(Garden &garden, Population &population, int (&arguments)[10]); bool Bee(Garden &garden, Population &population, int (&arguments)[10]); }; 

In diesen sekundÀren Funktionen konstruieren wir Funktionen, indem wir einfach andere Aufgaben verketten:

  • Die Gehaufgabe ist nur ein paar Schritte (mit Fehlerbehandlung)
  • Die Take-Aufgabe ist die Look-and-Swap-Aufgabe (sie wird aufgrund der Ant-Memory-Verarbeitung benötigt, die ich spĂ€ter erlĂ€utern werde).
  • Die Leerlaufaufgabe besteht darin, einen zufĂ€lligen Ort auszuwĂ€hlen und sich dorthin zu bewegen (zu Fuß), mehrere Zyklen zu warten (zu warten) und diesen Zyklus eine bestimmte Anzahl von Malen zu wiederholen
  • 
 usw

Andere Aufgaben sind komplizierter. Die Suchaufgabe fĂŒhrt eine Speicherabfrage aus, um nach Erinnerungen an Orte zu suchen, die das Objekt "food" enthalten (essbar fĂŒr diesen Bot-Typ). Sie lĂ€dt diese Erinnerungen herunter und geht sie alle um, „sucht“ nach Nahrung (bei Ameisen ist dies Gras). Wenn es keine Essenserinnerungen gibt, lĂ€sst die Aufgabe die Kreatur zufĂ€llig die Welt durchstreifen und sich umschauen. Durch Beobachten und Studieren (durch einen „Blick“ mit viewRadius = 1, dh nur auf die darunter liegende Kachel) kann die Kreatur ihr GedĂ€chtnis mit Informationen ĂŒber ihre Umgebung aktualisieren und intelligent und gezielt nach Nahrung suchen.

Eine allgemeinere Futteraufgabe besteht darin, Lebensmittel zu finden, Lebensmittel aufzunehmen, zu inspizieren (um das GedĂ€chtnis zu aktualisieren und Lebensmittel in der Nachbarschaft zu finden), nach Hause zurĂŒckzukehren und Lebensmittel zu lagern.


Möglicherweise stellen Sie fest, dass die Ameisen aus dem Ameisenhaufen herauskommen und gezielt nach Nahrung suchen. Aufgrund der Initialisierung wird der Anfangspfad der Ameisen zu einem zufÀlligen Punkt geleitet, da ihr Speicher bei t = 0 leer ist. Dann erhalten sie den Befehl, Lebensmittel in der Futteraufgabe aufzunehmen, und sie sehen sich auch um, um sicherzustellen, dass keine Lebensmittel mehr vorhanden sind. Von Zeit zu Zeit beginnen sie zu wandern, weil ihnen die Orte ausgehen, an denen sie Nahrung sahen (bedrohliche Kurzsichtigkeit).

Und schließlich hat der Bot eine „Ansicht“, die die Art der ihm zugewiesenen KI bestimmt. Jede Ansicht ist einer Steuerungsaufgabe zugeordnet, die ihr gesamtes Verhalten definiert: Sie besteht aus einer Kaskade von immer kleineren Aufgaben, die leicht durch eine Reihe von Speicherwarteschlangen und primitiven Aufgaben bestimmt werden können. Dies sind Aufgaben wie Ant und Bee.

Top-Down-Struktur


Wenn Sie von oben nach unten schauen, besteht das System aus einer Task-Master-Klasse, die Kontrollaufgaben und deren AusfĂŒhrung fĂŒr jeden einzelnen Bot auf der Karte koordiniert.

Der Taskmaster verfĂŒgt ĂŒber einen Vektor von Steuerungsaufgaben, von denen jede einem Bot zugeordnet ist. Jede Steuerungsaufgabe verfĂŒgt wiederum ĂŒber eine Warteschlange mit Unteraufgaben, die wĂ€hrend der ersten Initialisierung des Aufgabenobjekts mit der zugehörigen Aufgabenfunktion geladen werden.

 class Task{ public: //Members std::stack<Task> queue; bool initFlag = true; int args[10]; bool (Task::*handle)(Garden&, Population&, int (&)[10]); int botID; std::string name; //Constructor Task(std::string taskName, int taskBotID, bool (Task::*taskHandle)(Garden&, Population&, int (&)[10])){ name = taskName; botID = taskBotID; handle = taskHandle; } //Launch a Task bool perform(Garden &garden, Population &population); //Continue with primitives here... 

Jedes Task-Objekt in der Warteschlange speichert ein Array von Argumenten, das an den zugehörigen Funktionshandler ĂŒbergeben wird. Diese Argumente bestimmen das Verhalten dieser primitiven Aufgaben, die so allgemein wie möglich erstellt werden. Argumente werden als Referenz ĂŒbergeben, sodass das Task-Objekt in der Warteschlange seine Argumente speichern und seine Unterfunktionen Ă€ndern kann, sodass Sie beispielsweise Iterationen implementieren können, um auf eine bestimmte Anzahl von Ticks zu warten, oder Anforderungen zum Sammeln einer bestimmten Anzahl von Elementen usw. Unterfunktionen Ă€ndern den Wert des Iterators (Argument [n]) der ĂŒbergeordneten Funktion als Referenz und machen seine Erfolgsbedingung von seinem Wert abhĂ€ngig.

In jeder Kennzahl geht der Taskmaster die Liste der Steuerungsaufgaben durch und fĂŒhrt sie durch Aufrufen ihrer Perform-Methode aus. Die perform-Methode betrachtet wiederum das oberste Element der Warteschlange innerhalb der Task und fĂŒhrt es mit den Argumenten aus der Task aus. Auf diese Weise können Sie die Warteschlange der Aufgaben kaskadieren und immer die höchste Aufgabe ausfĂŒhren. Dann bestimmt der RĂŒckgabewert der Aufgabe den Abschluss der Aufgabe.

 //Execute Task Function bool Task::perform(Garden &garden, Population &population){ //Debug Message if(debug){std::cout<<"Bot with ID: "<<botID<<" performing task: "<<name<<std::endl;} //Change the Name and Execute the Task population.bots[botID].task = name; return (*this.*handle)(garden, population, args); } 

Wenn eine primitive Aufgabe true zurĂŒckgibt, hat sie ihren stabilen Punkt erreicht oder sollte zumindest nicht wiederholt werden (z. B. gibt step true zurĂŒck, wenn die Kreatur den Endpunkt erreicht hat). Das heißt, seine RĂŒckgabebedingung ist erfĂŒllt und es wird aus der Warteschlange entfernt, damit die nĂ€chste Aufgabe in der nĂ€chsten Maßnahme abgeschlossen werden kann.

Eine Aufgabe, die eine Aufgabenwarteschlange enthĂ€lt, gibt true zurĂŒck, nachdem die Warteschlange leer ist. Dank dessen ist es möglich, komplexe Aufgaben mit der Struktur von Warteschlangen und Unterwarteschlangen zu erstellen, in denen stĂ€ndig dieselben Funktionen aufgerufen werden, aber jeder Aufruf den Spielstatus und den Aufgabenstatus einen Schritt wiederholt.

Schließlich verwenden die Steuerungsaufgaben eine einfache Struktur: Sie werden in jedem Zyklus aufgerufen, laden die Aufgabe nur, wenn sie leer sind, und fĂŒhren ansonsten Aufgaben aus, die in ihre Warteschlange geladen wurden.

 //Species Functions bool Task::Ant(Garden &garden, Population &population, int (&arguments)[10]){ //Initial Condition if(initFlag){ Task forage("Search for Food", botID, &Task::forage); forage.args[0] = population.bots[botID].forage; //What are we looking for? queue.push(forage); initFlag = false; } //Queue Loop if(!queue.empty()){ //Get the Top Task Task newtask = queue.top(); queue.pop(); //If our new Task is not performed successfully if(!newtask.perform(garden, population)){ queue.push(newtask); return false; } //If it was successful, we leave it off return false; } //Return Case for Mastertask initFlag = true; return false; } 

Mit Hilfe meiner Warteschlangenschleife (siehe Code) kann ich wiederholt eine Funktion ausfĂŒhren und jedes Mal das oberste Element in der Warteschlange ausfĂŒhren, wobei Elemente aus der Warteschlange herausgeschoben werden, wenn der Aufruf ihrer perform-Methode true zurĂŒckgibt.

Ergebnisse


All dies ist in libconfig verpackt, sodass die Simulationsparameter sehr einfach geĂ€ndert werden können. Sie können viele Kontrollaufgaben problemlos codieren (ich habe Ameisen und Bienen erstellt), und das Definieren und Laden neuer Arten mit libconfig ist ĂŒberraschend einfach.

 //Anthill General Configuration File debug = true; //World Generation Parameters seed = 15; water = true; //Species that the simulation recognizes Species: { //Ant Species Ant: { masterTask = "Ant"; color = (0, 0, 0); viewDistance = 2; memorySize = 5; forage = 2; trail = true; fly = false; } Bee: { masterTask = "Bee"; color = (240, 210, 30); viewDistance = 4; memorySize = 30; forage = 4; trail = false; fly = true; } Worm: { masterTask = "Bee"; color = (255, 154, 171); viewDistance = 1; memorySize = 5; forage = 3; trail = true; fly = false; } } Population: ( {species = "Ant"; number = 40;}//, //{species = "Bee"; number = 12;}, //{species = "Worm"; number = 5;} ) 

Sie wurden elegant in die Simulation geladen. Dank einer neuen, verbesserten Suche nach Pfaden kann ich eine große Anzahl einzelner aktiver Bots simulieren, die Lebensmittel auf einer zweidimensionalen Ebene sammeln.


Simulation von 40 Ameisen, die gleichzeitig Gras sammeln. Die Wege, die sie im Sand schaffen, sind auf das erhöhte Gewicht zurĂŒckzufĂŒhren, das dem „unberĂŒhrten“ Land zugewiesen wird. Dies fĂŒhrt zur Schaffung charakteristischer "Ameisenautobahnen". Sie können auch als Pheromone interpretiert werden, aber es wĂ€re eher die Wahrheit, wenn die Ameisen tatsĂ€chlich Erinnerungen austauschen wĂŒrden.

Die ModularitĂ€t dieses Systems gewĂ€hrleistet die schnelle Schaffung neuer Arten, deren Verhalten durch eine einfache Kontrollaufgabe bestimmt wird. Im obigen Code können Sie sehen, dass ich WĂŒrmer und KI-Bienen erstellt habe, indem ich einfach ihre Farbe, PfadsuchbeschrĂ€nkungen (sie können nicht fliegen), Sichtbarkeitsbereich und SpeichergrĂ¶ĂŸe geĂ€ndert habe. Gleichzeitig habe ich ihr allgemeines Verhalten geĂ€ndert, da alle diese Parameter von Funktionen primitiver Aufgaben verwendet werden.

Debuggen von Ant Memories


Die Struktur komplexer Aufgaben und des GedĂ€chtnisses hat zu unvorhergesehenen Schwierigkeiten und der Notwendigkeit gefĂŒhrt, Ausnahmen zu behandeln.

Hier sind drei besonders komplexe Speicherfehler, die mich dazu gebracht haben, die Subsysteme zu wiederholen:

Ameisen laufen im Kreis


Einer der ersten KĂ€fer, denen ich begegnen musste: Ameisen rannten wie verrĂŒckt auf dem nackten Boden entlang des Musters auf dem Platz. Dieses Problem trat auf, weil ich zu diesem Zeitpunkt noch keine Speicheraktualisierung implementiert hatte. Die Ameisen hatten Erinnerungen an den Ort des Essens, und sobald sie das Gras aufnahmen und sich wieder umsahen, bildeten sich neue Erinnerungen.

Das Problem war, dass sich der neue Speicher am selben Punkt befand, der alte jedoch erhalten blieb. Dies bedeutete, dass sich Ameisen bei der Suche nach Nahrung an den Ort der Nahrung erinnerten und diesen behielten, der nicht mehr gĂŒltig war, aber diese alten Erinnerungen blieben erhalten und ersetzten neue (sie erinnerten sich an dieses köstliche Kraut).

Ich habe es wie folgt behoben: Die Daten des Objekts werden in alten Erinnerungen einfach ĂŒberschrieben, wenn wir denselben Ort sehen und sich das Objekt geĂ€ndert hat (zum Beispiel sieht die Kreatur, dass dort kein Gras mehr ist, erinnert sich aber nicht daran, dass es frĂŒher Gras gab). Vielleicht fĂŒge ich in Zukunft einfach die Eigenschaft "ungĂŒltig" zu den Erinnerungen hinzu, damit sich die Bots an alte Informationen erinnern können, die möglicherweise wichtig sind, aber die Informationen, die nicht mehr gĂŒltig sind, "aufgetaucht" sind ("Ich habe hier einen BĂ€ren gesehen, aber jetzt ist er weg").

Ameisen nehmen GegenstÀnde unter anderen Ameisen auf


Von Zeit zu Zeit (insbesondere bei einer großen Anzahl von Ameisen und einer hohen Grasdichte) können zwei Ameisen in einem Takt auf eine Grasfliese gelangen und versuchen, sie aufzunehmen. Dies bedeutete, dass die erste Ameise die Kachel betrat, sich umsah und den Gegenstand in 3 Schritten nahm. Die zweite Ameise tat dasselbe, nur kurz bevor sie das Objekt anhob. Eine andere Ameise riss es unter seiner Nase hervor. Er setzte seine Aufgaben ruhig fort, untersuchte dieselbe Umgebung wie die andere Ameise im vorherigen Takt und verarbeitete seine Speicherzeile auf dieselbe Weise (weil zu diesem Zeitpunkt ihre Erinnerungen identisch sind). Dies fĂŒhrte dazu, dass die zweite Ameise die erste kopierte, niemals GegenstĂ€nde aufnahm und der ersten folgte, was tatsĂ€chlich die ganze Arbeit erledigte. Ich bemerkte dies, weil in der Simulation der fĂŒnf Ameisen nur drei sichtbar waren. Es dauerte lange, bis die Ursache gefunden war.

Ich habe dieses Problem gelöst, indem ich die Swap-Aufgabe primitiv gemacht und die Take-Aufgabe erstellt habe, die zuerst auf den Boden schaut, um festzustellen, ob sich dort ein Objekt befindet. Wenn dies der Fall ist, „tauscht“ es und wenn nicht, „wartet“ es auf zwei ZĂŒge, sodass die andere Ameise definitiv geht. In einem Fall gilt diese Aktion fĂŒr zwei Maßnahmen, im anderen fĂŒr eine Maßnahme.

Nicht erreichbare Standorte


Ein weiterer unangenehmer Fehler, der mich zwang, die Verarbeitung des GedĂ€chtnisses zu wiederholen, war, dass einige Orte, die die Ameise sehen konnte, fĂŒr ihn unerreichbar waren. Sie entstanden aufgrund meiner faulen Platzierung von „Graskreuzen“ an Land, die manchmal ĂŒber dem Wasser hingen. Dies brachte mich dazu, die Schrittaufgabe zu verallgemeinern.

Bei der Übermittlung einer Anfrage zur Nahrungssuche hatten Ameisen oft Erinnerungen an Orte, die sie wirklich nicht erreichen konnten (sie sahen Gras ĂŒber dem Wasser und wollten es wahnsinnig sammeln). Wenn es nicht in ihrem Speicher markiert war (zum Beispiel die boolesche Variable „erreichbar“), erinnerten sie sich weiterhin daran und schrieben in die Warteschlange, bis diese Aktion die einzige war. Dies verursachte eine starke Hemmung, da sie bei jeder Maßnahme stĂ€ndig Pfadfindungsoperationen durchfĂŒhrten, versuchten, dorthin zu gelangen, und scheiterten .

Die Lösung bestand darin, den Speicher in der Schrittaufgabe zu aktualisieren, wenn der Pfad zum Ort nicht gefunden werden kann, und ihn im Speicher als nicht erreichbar zu markieren. DarĂŒber hinaus fragt die Suchaufgabe nur Orte mit Lebensmitteln nach erreichbaren Erinnerungen ab.

System im Allgemeinen


Im Allgemeinen möchte ich sagen - ja, ich bedauere, dass ich eine Woche meines Lebens mit einem Programmiermarathon verbracht habe, weil ich inspiriert wurde, Bots zu erstellen, die das tun, was ich ihnen sage (und auch, was sie tun wollen!). Ich musste ein paar Tricks machen und habe viel gelernt.

Das von mir erstellte System ist nicht 100% zuverlĂ€ssig und ich bemerke immer noch einige Artefakte. Als Richtung fĂŒr das Parsen des Looks wird die Aktion beispielsweise von oben nach unten und von links nach rechts verwendet, dh der letzte Speicher befindet sich in der unteren rechten Ecke. Wenn Sie Informationen abrufen, um nach GegenstĂ€nden zu suchen, bedeutet dies, dass sich Kreaturen nach SĂŒdosten bewegen. Dies macht sich insbesondere bei großen Simulationen bemerkbar, wenn das Gras unabhĂ€ngig vom Samen schnell wĂ€chst und sich leicht nach SĂŒdosten biegt.

Verbesserungen


Ich denke, dass signifikante Verbesserungen erforderlich sind, um komplexere Erinnerungen an komplexere Kreaturen zu simulieren.

Dies umfasst das Erhöhen der ZuverlĂ€ssigkeit von Speicherverarbeitungsfunktionen sowie das HinzufĂŒgen neuer Grundelemente wie „Denken“ und Ableitungen von Aufgaben auf hoher Ebene wie „Entscheiden“ oder „TrĂ€umen“. "Denken" kann eine primitive Aktion einer Speicheranforderung sein. Ein „Traum“ kann wiederum aus mehreren „Denk“ -Aufrufen bestehen: AuswĂ€hlen eines zufĂ€lligen Speichers, Erhalten einer zufĂ€lligen Eigenschaft und wiederholtes Wiederholen, um gemeinsame Themen oder wichtige Assoziationen zu verstĂ€rken.

FĂŒr die Zukunft plane ich drei spezifische ErgĂ€nzungen:

  • Interrupt-Behandlung und Aufgabenpriorisierung hinzufĂŒgen
  • FĂŒgen Sie die Kommunikation zwischen EntitĂ€ten hinzu
  • FĂŒgen Sie eine Gruppenstruktur hinzu, damit sich EntitĂ€ten formal identifizieren können

Die Unterbrechung der Verarbeitung und Priorisierung von Aufgaben kann fĂŒr die Interaktion zwischen EntitĂ€ten erforderlich sein, da der Bot seine AktivitĂ€ten nicht blind fortsetzen kann, wenn sie mit ihm kommunizieren (er muss irgendwie „zuhören“) oder angegriffen wird („weglaufen“ oder „kĂ€mpfen“). )

Die Kommunikation zwischen EntitĂ€ten besteht wahrscheinlich aus einer oder zwei primitiven Aufgaben zum Austauschen von Erinnerungen oder zum Anfordern von Erinnerungen an andere Bots (z. B. "Sagen" oder "Fragen"). Auf diese Weise können Informationen wie der Standort von Lebensmitteln oder andere Ressourcen ĂŒbertragen werden.

Ich hoffe, diese Aufgaben umzusetzen und ein Diagramm der Ressourcenakkumulationsrate einer großen Gruppe mit und ohne Kommunikation zu erstellen. Die Bevölkerung verfolgt bereits die Menge der in jeder Maßnahme gesammelten Lebensmittel. Es wĂ€re interessant zu zeigen, dass das Teilen von Erinnerungen die Effizienz beeintrĂ€chtigen kann.

Die Zukunft


Die wichtigste Funktion zur Simulation von Communities besteht darin, Gruppenstrukturen hinzuzufĂŒgen und diese Gruppen mit Eigenschaften auf Makroebene auszustatten, beispielsweise ihren gemeinsamen „Zielen und Verantwortlichkeiten“. Dies gibt uns eine Art „Keim“, aus dem wir Aufgaben auf hoher Ebene erhalten können, die in der Hierarchie der Gruppenstrukturen an Aufgaben auf niedrigerer Ebene delegiert werden, die sich direkt auf die Welt auswirken. Es ermöglicht Ihnen auch, eine Form der politischen Struktur zu schaffen.

Ein solches System ist völlig autark, und die Visualisierung wird einfach darĂŒber gelegt. Es wird sehr einfach sein, Insekten durch Humanoide zu ersetzen, Ressourcen zu sammeln und an einem Ort zu lagern, damit sie grĂ¶ĂŸer werden.Die Art des Wachstums ihres Hauses kann zum Beispiel sehr abhĂ€ngig oder völlig unabhĂ€ngig von den Aktionen von Bots sein. Verschiedene Arten können unterschiedliche StĂ€mme mit unterschiedlichen Merkmalen und Trends haben.

Außerdem kann ich dieses System mit zuvor erstellten Kartengeneratoren kombinieren (Erweiterung der Weltklasse), um die Welt realer zu machen.

Abschließend


In naher Zukunft plane ich, die Kreaturen durch Menschen zu ersetzen und einige der letzten Funktionen zu implementieren. Vielleicht werde ich den vollstÀndigen Quellcode veröffentlichen, wenn ich die QualitÀt des Systems verbessere (an einigen Stellen ist der Code ziemlich chaotisch).

Warten Sie auf den nÀchsten Artikel. In der Zwischenzeit ist hier ein Video mit Bienen, die nach Pollen in Blumen suchen. Sie werden mit demselben Framework codiert.


Ich habe diesen Samen gewĂ€hlt, weil der Ausgangspunkt auf einer kleinen Insel liegt. Die Bienen sind jedoch nicht so programmiert, dass sie in den Bienenstock zurĂŒckkehren, sondern sammeln stĂ€ndig Pollen. Möglicherweise stellen Sie fest, dass ihre Sichtweite höher ist und sie sich manchmal sehr absichtlich zu der Blume bewegen, die sie gerade gesehen haben.

... und hier ist die Bee Task-Mitgliedsfunktion:

 bool Task::Bee(Garden &garden, Population &population, int (&arguments)[10]){ //Just Search for Flowers if(initFlag){ //Define our Tasks Task take("Take Food", botID, &Task::take); Task eat("Eat Food", botID, &Task::consume); Task search("Locate Food", botID, &Task::search); search.args[0] = population.bots[botID].forage; queue.push(eat); queue.push(take); queue.push(search); initFlag = false; } //Work off our allocated queue. if(!queue.empty()){ //Get the Top Task Task newtask = queue.top(); queue.pop(); //If our new Task is not performed successfully if(!newtask.perform(garden, population)){ //Put the Task back on queue.push(newtask); } //If it was successful, we leave it off return false; } initFlag = true; return true; } 

Source: https://habr.com/ru/post/de443252/


All Articles