Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

ss19:finaler_code

Dies ist eine alte Version des Dokuments!


Unser Code steht in 4 Versionen zur Verfügung:

1. neuronal_infinity_map.zip
Das Verhalten der Kreaturen dieser Version geschieht auf Basis neuronaler Netze. Die Karte ist „unendlich“ groß, d.h. wenn die Kreaturen die Mapgrenze überqueren, tauchen sie auf der anderen Seite der Map wieder auf.

2. polynom_infinity_map.zip
Das Verhalten der Kreaturen dieser Version geschieht auf Basis von Polynomen. Die Karte ist „unendlich“ groß, d.h. wenn die Kreaturen die Mapgrenze überqueren, tauchen sie auf der anderen Seite der Map wieder auf.

3. neuronal_map_limits.zip
Das Verhalten der Kreaturen dieser Version geschieht auf Basis neuronaler Netze. Die Karte ist „limitiert“, d.h. wenn die Kreaturen die Mapgrenze überqueren, sterben sie.

4.polynome_map_limits.zip
Das Verhalten der Kreaturen dieser Version geschieht auf Basis neuronaler Netze. Die Karte ist „limitiert“, d.h. wenn die Kreaturen die Mapgrenze überqueren, sterben sie.

Alle vier Versionen bestehen aus den Dateien map, player und constants. Bei den Versionen auf Basis neuronaler Netze gibt es zudem noch die Datei neuronal, die eben dieses Netz aufbaut.
Für alle Versionen werden einige Pakete benötigt. Das Paket Pygame sorgt für die graphische Darstellung der Kreaturen und des Futters. Mit matplotlib.pyplot werden die Statistiken erstellt. Die Pakete math und random sind für die zufälligen Komponenten und einige Berechnungen. Mit copy werden die Kreaturen kopiert. Und numpy wird für das neuronale Netz benötigt.
Das Kernstück des Programms ist die Map. Wir teilen diese in verschiedene gleichartige Chunks auf, um die Rechenzeit zu minimieren. Jeder Chunk hat dabei seine Position (x und y Koordinaten der linken unteren Ecke), seine Länge und Höhe, welche in der Regel den gleichen Wert haben und die anfängliche Wahrscheinlichkeit, das Futter erscheint, Fooddensity genannt. Zudem gibt es die Möglichkeit, die Chunks zu benennen und sie sich überlappen zu lassen. Desweitern ist in jedem Chunk noch eine Liste mit den im Chunk vorhandenen Futter angelegt. Vergleiche hier die Initialisierungsfunktion der Chunks:

def __init__(self,x,y,length,height,overlay,name):
        self.x = x
        self.y = y
        self.length = length
        self.height = height
        self.overlay = overlay
        self.objects = [] #Liste der festen Objekte
        self.density = FOODDENSITY #Wahrscheinlichkeit für neue Objekte        
        self.name = name

Die Objekte werden durch die Funktion generate() erstellt und zu der Objektliste hinzugefügt. Es gibt über die Variable multi die Möglichkeit, hier noch mal die Menge des Futters in diesem Chunk zu verändern.

#generiert zufällig verteilte Objekte in dem Chunk
    def generate(self,multi=1):
        a = Object(0,0,0)
        numb = self.length*self.height*self.density/a.size*multi
        for i in range(int(numb)):
                    self.objects.append(Object(random.randint(self.x+int(self.height*0.1),self.getendx()-int(self.height*0.1)-a.size),random.randint(self.y+int(self.height*0.1),self.getendy()-int(self.height*0.1)-a.size),i))

Die Objekte werden durch die Klasse Object erstellt. Alle Objekte haben ihre Koordinaten (x und y), welche sich auch nicht verändern, es gibt also keine sich bewegenden Futterstücke. Zudem haben die Objekte einen Status, also ob sie schon gefressen wurden, einen Stamina Wert der sich aus ihrer Größe und dem Grundfutterstaminawert ergibt und der Kreaturen gutgeschrieben wird, die das Futterstück fressen und ihre rect() Funktion, welche für Pygame benötigt wird. Vergleiche hierfür die Initialisierungsfunktion:

#Initialisiert das Objekt
    def __init__(self,x,y,name):
        self.x=x #x-Pos
        self.y=y #y-Pos
        self.status = True #Aktiv oder nicht
        self.name = name #ID
        self.size = random.randint(3,5) #Größe
        self.rect = pygame.Rect(self.x, self.y, self.size,self.size) #Rechteck in Pygame
        self.stamina=FOOD_STAMINA*self.size

Zudem haben die Objekte noch die Möglichkeit zu mutieren. Dies ist für den Fall gedacht, dass nicht jede Generation gleich viele Objekte generiert werden sollen, sondern die Objekte aus den nicht gefressenen Objekten der vorherigen Generation erzeugt werden. Bei der Mutation wird einerseits die Größe der Objekte verändert und andererseits ein zufälliger neuer Punkt für das neue Objekt innerhalb eines Quadrates der Seitenlänge xh um das alte Objekt gewählt. Zudem wird wieder für Pygame die rect() Funktion eingefügt.

#Mutiert die Objekte
    def mutate(self,xmin,xh):
        self.size=self.size+random.choice(range(-9,10))*0.01  #Mutiert Größe
        self.x=self.x+random.choice(range(xmin,xmin+xh))      #legt x und 
        if self.x<0:
            self.x=self.x*(-1)
        self.y=self.y+random.choice(range(xmin,xmin+xh))      #y in der Nähe der Werte des Vorbilds fest
        if self.y<0:
            self.y=self.y*(-1)
        self.rect = pygame.Rect(self.x, self.y, self.size,self.size)  


Die Karte selber, zu welcher sich die Chunks ja zusammensetzen, hat ebenfalls einige Eigenschaften. Da wäre zunächst ihre als Mapsize bezeichnete Seitenlänge (Da die Karte ja quadratisch ist, gibt es nur eine Seitenlänge). Dann die Größe der Chunks und wie sehr sich diese Überlappen können (overlap). Desweiteren ist in der Karte gespeichert, welche Spieler sich gerade auf ihr bewegen. Für jeden dieser Spieler ist die Welt die Karte.

#Initilisiert die Map
    #def__init__(Liste der Objekte,Liste der Spieler,Gesamtgröße der Map,Größe der Chunks,Liste der alten Objekte)
    def __init__(self,playerlist,size,sizechunk,old_objects):
        self.worldsize = size #Worldsize
        self.chunksize = sizechunk #Chunksize wird später static
        self.overlay = sizechunk*0.2 #Overlay der Chunks
        self.player = playerlist #Spielerliste
        for player in self.player:
            player.world = self
        self.maplist = [] #2-Dim Liste der Chunks
        self.initiate(old_objects)
        self.playerupdate()

Wie in dem Code bereits zu sehen gibt es noch zwei für die Initialisierung der Karte wichtige Funktionen der Karte. Da wäre zunächst die initiate() Funktion. In sie wird eine Liste der nicht gefressenen Objecte der vorherigen Generation eingegeben, bzw. wenn es keine vorherige Generation gab, eine 0. In initiate() wird zunächst die Anzahl der Chunks auf der Karte pro Reihe errechnet, der factor. Dann werden die Chunks der Reihe nach durchlaufen, von unten nach oben und von links nach rechts. Es werden dann nach der oben beschriebenen Funktion die Chunks generiert. Wenn entsprechend eingestellt, wird Futter in der Umgebung des überlebenden Futters aus der letzten Runde generiert, d.h. solange old_objects ungleich 0 ist, werden die einzelnen Objekte durchgegangen und alle, die in dem gerade betrachteten Chunk waren, werden kopiert, mutiert und der neuen Objektliste des Chunks hinzugefügt. Des weiteren wird noch eine kleine Menge Futter zufällig neu generiert. Sollte deaktiviert sein, dass das Futter aus dem alten Futter generiert wird, wird das Futter jede Generation neu generiert.

#Initilisiert die Grundmap, sowie weist jedem Chunk die Objekte hinzu
    def initiate(self,old_objects):
        factor = int(self.worldsize/self.chunksize)
        #print(factor)
        R = 0
        a=0
        u=0
        #Durchläuft die y-Richtung der Chunks
        for i in range(factor):
            line = []
            a+=1
            qqq = 110351524512345
            qqqq = 12345
            #Durchläuft die x-Richtung der Chunks
            for e in range(factor): 
                u+=1
                line.append(Chunk(self.chunksize*e,self.chunksize*i,self.chunksize,self.chunksize,self.overlay,e+i*factor))
                #Wenn entsprechend eingestellt, wird Futter in der Umgebung des überlebenden Futters aus der letzten Runde generiert
                if FOOD_GENERATION_PER_OLD_FOOD:
                    if old_objects==0:     #Für die erste Runde wird in jedem Chunk entsprechend Futter generiert
                        line[e].generate()
                    else:       
                        objects=[]   #die Liste der zu kopierenden Objekte in dem Chunk
                        for o in old_objects:
                            if o.x<self.chunksize*(e+1) and o.y<self.chunksize*(i+1):
                                if o.x>=self.chunksize*e and o.y>=self.chunksize*i:
                                    objects.append(o)
                        #kopiere die Objekte so oft, wie es der geamten Anzahl der Objekte angemessen ist
                        for o in objects:
                            if (MAPSIZE/CHUNKANZAHL)<len(objects):
                                N=random.randint(0,2)
                            elif (MAPSIZE/CHUNKANZAHL)/2<len(objects):
                                N=random.randint(0,4)
                            else:
                                N=random.randint(0,6)
                            n=0

                            while n<N:
                                ob=copy.copy(o)
                                ob.mutate(-20,40)
                                line[e].objects.append(ob)

                                n+=1
                        #Füge in jedem Chunk noch mal eine kleine Anzahl Objekte hinzu
                        line[e].generate(FOOD_PER_GENERATION)
                
                else:
                    if random.random()<0.7:
                        line[e].generate()
                
            self.maplist.append(line)
            del line
        

Die andere bereits in der Initialisierung verwendete Funktion ist die playerupdate() Funktion. Diese unterscheidet sich, je nachdem ob man eine limitierte map vorliegen hat oder eine unbegrenzte. Ist die Map limitiert, wird die Liste der Spieler durchgegangen und geprüft, ob sich diese noch innerhalb der Mapgrenzen befinden. ist dies nicht der Fall, so stirbt die Kreatur.

#überprüft die Chunkposition des Spielers
    def playerupdate(self):

        #Durchläuft die Liste der Player
        q=0
        while q < len(self.player):

            #Chunkposition in der Liste
            i = self.player[q].chunkpos[0]
            e = self.player[q].chunkpos[1]
        
            x = self.player[q].x #x-Pos des Spielers
            y = self.player[q].y #y-Pos des Spielers
            x0 = 0 #kleinste x-Pos der Chunks
            y0 = 0 #kleinste y-Pos der Chunks
            xn = self.worldsize #größte x-Pos der Chunks
            yn = self.worldsize #größte x-Pos der Chunks
            
            
            if x <= x0 or x >= xn or y <= y0 or y >= yn:
                self.player[q].alive=False
            else:
                #Da sich die Map auch in den negativen Bereich erweitert, erhalten alle Chunks einen Shift.
                self.player[q].chunkpos[1] = int(x/self.chunksize)
                self.player[q].chunkpos[0] = int(y/self.chunksize)
                
            q+=1

Ist die Map hingegen unbegrenzt, so wird, sollte die Kreatur außerhalb der Mapgrenzen sein, diese in den gegenüberliegenden Chunk gesetzt.

#überprüft die Chunkposition des Spielers
    def playerupdate(self):

        #Durchläuft die Liste der Player
        q=0
        while q < len(self.player):

            #Chunkposition in der Liste
            i = self.player[q].chunkpos[0]
            e = self.player[q].chunkpos[1]
        
            x = self.player[q].x #x-Pos des Spielers
            y = self.player[q].y #y-Pos des Spielers
            x0 = 0 #kleinste x-Pos der Chunks
            y0 = 0 #kleinste y-Pos der Chunks
            xn = self.worldsize #größte x-Pos der Chunks
            yn = self.worldsize #größte x-Pos der Chunks
            
            
            if x <= x0 or x >= xn or y <= y0 or y >= yn:
                self.player[q].alive=False
            else:
                #Da sich die Map auch in den negativen Bereich erweitert, erhalten alle Chunks einen Shift.
                self.player[q].chunkpos[1] = int(x/self.chunksize)
                self.player[q].chunkpos[0] = int(y/self.chunksize)
                
            q+=1

Erzeugt wird die Map in der Funktion simulate_one_round(). Diese hat als Eingabe die Liste der mutierten Kreaturen und die Liste der Objekte aus der vorherigen Generation. Nach dem erstellen der Map laufen die Kreaturen solange auf dieser herum, bis alle Tod sind. Dabei tut jede Kreatur bei jedem Durchlauf der while-Schleife einen Schritt. Dazu wird zunächst die ganze Karte mit der Hintergrundfarbe „übermalt“. Danach wird die bereits beschriebene Funktion playerupdate() ausgeführt, danach wird mit der Funktion hitbox() geprüft, ob die Kreaturen entsprechend stehen, dass sie Futter einsammeln und dann werden die Objekte mit drawobj() gezeichnet. Es wird nun bei jeder Kreatur die Stamina geprüft, und sollte diese zu niedrig sein, stirbt die Kreatur. Sollte die Kreatur dann noch leben, wird sie in einer zu ihrem Staminawert passendem Blauton gezeichnet (Dunkel: Hohe Stamina, Hell: Niedrige Stamina). Dann werden die Kreaturen über die in den Kreaturen vorhandenen move() Funktionen bewegt und zu guter Letzt wird noch einmal geprüft, ob noch wenigstens eine Kreatur lebt. Ist dies nicht der Fall, so wird die Schleife abgebrochen, sonst Fängt sie wieder von vorne an.

def simulate_one_round(playerl,objects):
    global screen
    
    clock = pygame.time.Clock()

    map01 = Map(playerl,MAPSIZE,MAPSIZE//CHUNKANZAHL,objects)#erstellt die Map

    moveframes = 5 #Anzahl der Frames
    runde = 0 #Zählt die Schleifendurchläufe
    done = False #Falls True beendet das Programm
    winner = []
    while not done:
        #clock.tick(50) #Für größere Pausen zwischen den Runden

        #Beendet das Programm falls ...
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                done =True

        screen.fill((0,255,0)) #Setzt die Hintergrundfarbe
        map01.playerupdate() #Überprüft alle Spieler auf ihren Chunk
        map01.hitbox() #Überprüft die Hitbox zu allen Objekten in diesem Chunk
        map01.drawobj() #Zeichnet alle Objekte
        for player in playerl:
            if player.stamina<0:
                player.alive=False
        
        #Zeichnet den Spieler mit einer Farbe, abhängig von dem Ausdauer des spielers
        for player in playerl:
            if player.alive:
                if player.stamina<player.max_stamina:
                    b=255-255/(player.max_stamina)*player.stamina
                    
                else:
                    b=0
                
                pygame.draw.rect(screen, (0,b,b),player.rect)
        
        #Bewege die Spieler
        for player in map01.player:
            if player.alive:
                player.move()
                
        #Bei keine Spieler wird das Programm beendet
        if map01.get_number_of_living_players()==0:
            
            break
        
        pygame.display.flip()
        #time.sleep(20)
        runde += 1
    
    print("Eine Simulationsrunde ist beendet")#Programm ist beendet
    return map01

Ein paar Worte zu der Funktion hitbox(): Diese unterscheidet sich abhängig davon, ob die map unendlich oder endlich ist. In beiden Versionen wird für jede Kreatur eine Liste mit den Objekten aus dem Chunk in dem sich diese Kreatur befindet und den an diesen Chunk angrenzenden Chunks erstellt. Dann wird geprüft, ob die Kreatur mit einem oder mehreren der Futterstücke „kollidiert“, also ob sie diese frisst. Der Unterschied liegt in dem Erstellen dieser Listen mit Objekten. In der begrenzten Version wird für jeden angrenzenden Chunk geprüft, ob er sich von den Koordinaten des Chunks her auf der Karte befindet.

    def hitbox(self):
        #Durchläuft jeden Spieler
        for p in self.player:  #erstellt eine liste, in der alle Objekte des Chunks des Spielers und der Umgebenden Chunks gespeichert sind
            if p.alive:
                obj_list=[]
                q = p.chunkpos[0]
                w = p.chunkpos[1]
                #Fügt der Liste Objekte aus Chunks zu, welche an den Chunk des spielers angrenzen 
                #und nicht außerhalb der Map liegen
                for ob in self.maplist[q][w].objects:
                    obj_list.append(ob)

                if q-1>=0:
                    for ob in self.maplist[q-1][w].objects:
                        obj_list.append(ob)
                    if w-1>=0:
                        for ob in self.maplist[q-1][w-1].objects:
                            obj_list.append(ob)

                if w-1>=0:
                    for ob in self.maplist[q][w-1].objects:
                        obj_list.append(ob)
                    if q+1<int(self.worldsize/self.chunksize):
                        for ob in self.maplist[q+1][w-1].objects:
                            obj_list.append(ob)

                if q+1<int(self.worldsize/self.chunksize):
                    for ob in self.maplist[q+1][w].objects:
                        obj_list.append(ob)
                    if w+1<int(self.worldsize/self.chunksize):
                        for ob in self.maplist[q+1][w+1].objects:
                            obj_list.append(ob)

                if w+1<int(self.worldsize/self.chunksize):
                    for ob in self.maplist[q][w+1].objects:
                        obj_list.append(ob)
                    if q-1<=0:
                        for ob in self.maplist[q-1][w+1].objects:
                            obj_list.append(ob)

Bei der unendlichen Version wird hingegen, sollte der Chunk außerhalb der Kartengrenzen sein, der auf der gegenüberliegenden Seite liegende Chunk auf Futterstücke geprüft.

    #Hitboxberechung der Objekte und den Spielern
    def hitbox(self):
        #Durchläuft jeden Spieler
                        
            
        for p in self.player:  #erstellt eine liste, in der alle Objekte des Chunks des Spielers und der Umgebenden Chunks gespeichert sind
            if p.alive:
                obj_list=[]
                q = p.chunkpos[0]
                w = p.chunkpos[1]
                #Wenn der entsprechende Neben dem Chunk des Spielers liegende Chunk außerhalb der Karte liegen sollte, 
                #wird stattdessen der an Chunk genommen, in den die Kreatur bei einer Grenzübertretung eintreten würde
                if q-1<0:
                    q0 = int(self.worldsize/self.chunksize)-1
                else:
                    q0=q-1
                    
                if w-1<0:
                    w0 = int(self.worldsize/self.chunksize)-1
                else:
                    w0=w-1
                    
                if q+1>=int(self.worldsize/self.chunksize):
                    q2 = 0
                else:
                    q2=q+1
                    
                if w+1>=int(self.worldsize/self.chunksize):
                    w2 = 0
                else:
                    w2=w+1
                    
                for ob in self.maplist[q][w].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q][w0].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q][w2].objects:
                    obj_list.append(ob)
                
                for ob in self.maplist[q0][w].objects:
                    obj_list.append(ob)

                for ob in self.maplist[q0][w0].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q0][w2].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q2][w].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q2][w0].objects:
                    obj_list.append(ob)
                    
                for ob in self.maplist[q2][w2].objects:
                    obj_list.append(ob)

Der Rest läuft bei beiden Versionen gleich ab, die noch nicht gefressenen Objekte die sich in Reichweite der Kreatur befinden werden darauf geprüft, ob sie mit der Kreatur „kollidieren“. Ist dies der Fall, frisst die Kreatur sie.

                #überprüft die Objekte der Liste auf ihre erreichbarkeit in einem Schritt und ob sie noch nicht gefressen wurden
                for ob in obj_list:
                    if (p.x-ob.x)**2+(p.y-ob.y)**2<(p.speed*10)**2 and ob.status:
                        #Prüft dann, ob der Spieler mit dem Objekt kollidiert
                        if p.rect.colliderect(ob.rect):  
                            #Frißt dann das Objekt
                            ob.status = False

                            p.stamina+=ob.stamina
                            p.collected_food+=1

Die Funktion simulate_one_round() gibt schlussendlich die map zurück, und zwar an den Hauptcode. Dieser besteht im Grunde aus zwei while Schleifen. Die erste durchläuft die angegebene Anzahl an Durchläufen des Codes, die zweite durchläuft die angegebene Anzahl an Generationen. Vor jedem Durchlauf werden erst noch einige Zähler und die Statistiken vorbereitet. Außerdem wird durch die Funktion make_first_generation() die erste Generation Kreaturen erschaffen. Dafür werden so viele Kreaturen erschaffen wie vorgegeben und in einer Liste (playerl) gespeichert.

a=0
while a<5: #Anzahl der Durchläufe des Programms
    if __name__=="__main__":
        n=0                        #Generationen -Zähler
        best=0                     #Zähler für die am längsten überlebende Kreatur
        list_generation_number=[]  #Liste für die Generationennummer
        
        pygame.init()
        screen = pygame.display.set_mode((MAPSIZE,MAPSIZE)) #erstellt den Hauptbildschirm
        
        playerl = make_first_generation()
        
        survived_objects=0  #für die erste Generation Kennzeichen, dass es noch keine vorherige Objekte gibt
        #für spätere Generationen eine Liste mit den überlebenden Objekten
        
        list_menge=[]         #Liste der anzahl der Kreaturen in jeder Generation
        list_object_menge=[]  #Liste der anzahl der Objekte in jeder Generation
        
        #erstellen von Objekten für Statistiken. Der angegebene "Name" gibt jeweils die betrachtete Eigenschaft an. Achtung: Richtig schreiben.
        speed_statistic=statistic("speed")
        size_statistic=statistic("size")
        sight_statistic=statistic("sight")
        fat_statistic=statistic("fat")
        survived_steps_statistic=statistic("survived_steps")

Darauf folgt die Generationen Schleife. Diese läuft so lange, bis die Spezies ausstirbt, maximal aber über so viele Generationen wie vorgegeben (hier: 1000 Generationen). In jeder Generation wird die Funktion simulate_one_round() einmal ausgeführt, um die entsprechende Generation zu simulieren.

ss19/finaler_code.1566327329.txt.gz · Zuletzt geändert: 2019/08/20 20:55 von leanderh