Beginner braucht Tipps um Dungeoncrawler auf einen mehr "Pythonic-Way" zu optimieren :)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hallo zusammen

Mein Name ist Christian und ich bin hobbymässig am Python üben. Damit das Ganze auch so richtig Spass macht, habe ich direkt mit einem Game angefangen. Genauer gesagt ich bin dabei einen kleinen ASCII-Dungeoncrawler zu schreiben.

Vor einiger Zeit habe ich bereits einmal in diesem Forum gepostet. Ich habe damals sehr coole Typs (insbesondere von BlackJack) erhalten, wie ich meinen Code (in einem anderen Beispiel) verbessern kann. Ich würde mich auch bei meinem jetzigen kleinen Projekt (ASCII-Dungencrawler) über Feedbacks von euch zwecks Verbesserung meines Codes freuen.

Hier mein Code:

Code: Alles auswählen

class Character:
    """ Erstellt ein Character (Monster oder Player) """
    
    def __init__(self, name, symbol, position = (0,0)):
        self.max_hitpoints = 50
        self.hitpoints = self.max_hitpoints
        self.attack_power = 5
        self.position = position
        self.name = name
        self.symbol = symbol
        
    def attack(self, enemy):
        pass

class Grid:
    """erstellt ein Grid
       es muss Grösse in Form einer Liste (x,y) als Parameter angegeben werden"""
    
    def paint_grid(self):
        """ Zeichnet das Grid inkl. Character-Objecte auf den Bildschirm """
        # Zeichnet leeres Grid
        self.grid = []
        for column in range(self.y):
           self.grid.append([]) 
           for row in range(self.x):
               self.grid[column].append("_")
               
        # Fügt Wände dem Grid hinzu
        wall_x = 0
        wall_y = -1
        for row in self.wallist:
            wall_y += 1
            wall_x = -1
            for wall in row:
                wall_x += 1
                if wall == 1:
                    self.grid[wall_y][wall_x] = '#'
        
        # Fügt Character-Objekte dem Grid hinzu       
        for monster in self.monsterlist:
            monster_x, monster_y = monster.position
            self.grid[monster_x][monster_y] = monster.symbol
        # Print Grid on Screen
        for liste in self.grid:
            for item in liste:
                print(item, end = " ")
            print()
    
    def import_monster(self, monster):
        """ Importiert Monster in das Grid, es muss als Paramter ein Monsterobject angegeben werden"""
        for item in self.monsterlist:
            if monster.position == item.position:
                raise Exception ("There is already annother Monster on that position")
        self.monsterlist.append(monster)
        
    def remove_monster(self, monster):
        """ Löscht Monster vom Grid, es muss als Paramter ein Monsterobject angegeben werden"""
        for item in self.monsterlist:
                monsterlist.pop(monster)
        
    def show_monster(self):
        """ Zeigt alle Monster auf dem Grid """
        counter = 1
        for monster in self.monsterlist:
            print("Monster Nr. {}, Name: {}, Position: {}".format(counter, monster.name, monster.position))
            counter += 1
        self.paint_grid()
        
    def import_walls(self, walls):
        """ Importiert eine Walls-Liste """
        self.wallist = walls
            
            
    def __init__(self, grid_size):
        self.x, self.y = grid_size
        self.monsterlist = []
        self.walllist = []
        

""" Testing """

oggar = Character("Oggar", "O", (2,2))
rudolf = Character("Rudolf", "R", (3,3))

# walls ist eine liste mit weiteren listen. diese listen stellen praktisch das spielfeld
# dar. 1 = solid wall, 0 = keine wall
walls = [
    [1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 0, 0, 0, 1],
    [1, 1, 1, 1, 1]
    ]

map = Grid((5,5))

map.import_monster(oggar)
map.import_monster(rudolf)
map.import_walls(walls)

map.paint_grid()
Ich habe eine Klasse Character, mit welcher jeweils Monster erstellt werden. Die Klasse Grid importiert dann die Monster und stellt sie auf dem Grid dar. Die Klasse Grid hat noch eine Methode, welche Wände importiert, welche in einer Variable des Typs Liste gespeichert sind. 1 = Wand, 0 = keine Wand. Ich habe das Gefühl, dass ich den Code in der Klasse Grid unterhalb des Kommentars (Fügt Wände dem Grid hinzu) viel zu kompliziert geschrieben habe. Wie könnte ich das besser lösen?

Ich bin für alles Tipps dankbar. Lieber Gruss, Christian
Zuletzt geändert von Anonymous am Mittwoch 8. Juni 2016, 14:45, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da laesst sich einiges vereinfachen. Du musst zb nicht alles neu aufbauen, sondern kannst direkt den Dungeon durch Iteration ueber die Zeilen malen. Und dann an den passenden Stellen ein Monster, wenn du eine entsprechende Datenstruktur aufbaust:

Code: Alles auswählen

GRID = [
    [1, 1, 1, 1],
    [1, 0, 1, 1],
    [1, 0, 0, 0],
    [1, 1, 1, 1],
]

def draw_playfield(dungeon, monsters):
    monster_coords2monster = {(m.y, m.x): m.symbol for m in monsters}
    for y, row in enumerate(dungeon):
        for x, wall in enumerate(row):
            c = "#" if wall else monster_coords2monster.get((y, x), " ")
            print c,
        print


class Monster(object):

    def __init__(self, pos, symbol="@"):
        self.x, self.y = pos
        self.symbol = symbol

def main():
    monsters = [Monster((1, 1)), Monster((2, 2), "X")]
    draw_playfield(GRID, monsters)


if __name__ == '__main__':
    main()
Zuletzt geändert von Anonymous am Mittwoch 8. Juni 2016, 22:19, insgesamt 2-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hallo Deets, hallo BlackJack

Das Wichtigste zuerst: Vielen herzlichen Dank, dass du dir die Zeit genommen hast, meinen Code zu überarbeiten. Es hilft mir sehr, wenn ich zuerst selber den Code schreibe und dann eine optimierte Version bekomme. Insbesondere beeindruckt hat mich, dass dein Code soviel kürzer/effizienter als meiner ist.

Ich habe deinen Code einmal einfach 1 zu 1 in ein File (dungeoncrawler_enhanced.py) kopiert und mit dem Python3-Interpreter ausgeführt. Im Terminal habe ich dann folgendes erhalten:

weitnow: ~ $ python3 -i crawler_enhanced.py
#
#
#
#
#
@
#
#
#

X

#
#
#
#

Ich habe mir dann deinen überarbeiteten Code angesehen und zu verstehen versucht. Ich gehe einmal der Reihe nach durch, wie der Code nach meiner Meinung vom Intepreter ausgeführt wird:

In def main() wird eine Liste mit zwei Monsterobjekten erzeugt. Das erste Monsterobjekt hat Position (1,1) und es wird das Standardsymbol @ verwendet. Das zweite Monster hat Position (2,2) und übergibt der Klasse Monster das Symbol X als Parameter. Dann wird die Methode draw_playfield mit der Liste GRID und der Liste Monsters als Parameter aufgerufen.

In der Methode draw_playfield steht dann:
monster_coords2monster = { (m.y, m.x): m.symbol for m in monsters }

wenn ich mir nun Monster_coords2monster anzeigen lasse, dann sehe ich:
{(1, 1): '@', (2, 2): 'X'}

Es wurde also ein Dict mit diesem Inhalt erzeugt. Finde ich einfach den Hammer, dass du das mit einem so kurzen
Code wie { (m.y, m.x): m.symbol for m in monsters } erstellen kannst. Das muss ich mir merken :D (Python is soooo cool).

Auch die enumerate-Methode ist sehr cool...kannte ich vorher gar nicht.

Ich habe jetzt auch herausgefunden, dass dein Code vermutlich für den Python2 Intepreter geschrieben wurde?
Ich habe einfach print c in print(c, end = "") abgeändert und nun funktioniert alles.

Output:
weitnow: ~ $ python3 -i crawler_enhanced.py
####
#@##
# X
####

Also nochmals vielen lieben Dank...du hast mir sehr weiter geholfen....und der Code ist jetzt viel besser....ich werde einmal weiter dran rumbasteln.

Danke dir und eine schöne Woche, Grüsse, Christian
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hallo Deets

Nochmals ich :)

Ich habe mir heute nochmals deine Code genau angesehen....Dank deinen Verbesserungen und deiner Art um Liste, Dictionaries etc zu erzeugen hat mein Python-Wissen gefühlt wieder einen Sprung nach vorne gemacht. Ich wollte nochmals Danke sagen....ist ja wirklich nicht selbstverständlich, dass sich jemand in seiner Freizeit die Mühe macht, anderen zu helfen.
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

@weitnow: schoen, dass es geklappt hat. Und genau, ich hab' Python2 verwandt.
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hallo zusammen

Ich habe nochmals eine allgemeine Frage. Ich habe immer grosse Probleme zu verstehen, in welche Klasse ich jeweils eine Methode anhängen soll.

Um besser zu illustrieren was ich meine nehme ich gleich mein kleinen Dungeon Crawler als Beispiel:

Ich habe eine Klasse Grid oder Map. Diese Klasse übernimmt die Darstellung des Spielfelds/Dungeon und zeichnet auf der Konsole ein Grid mit Wänden und Monstern, sowie den Spieler.

Dann habe ich eine Klasse Monster, welche Attribute hat wie Hitpoints, Angriffstärke und Methoden wie Angriff etc.

Nun möchte ich natürlich, dass sich die Monster auf dem Grid/Map bewegen können. Also schreibe ich eine Methode Bewegen. Spontan würde ich sagen, dass diese Methode natürlich zu der Klasse Monster gehört, da Bewegen ja eine Fähigkeit des Monsters ist.

Allerdings verwaltet in meinem Beispiel die Klasse Grid/Map die Position der Wände. Damit ein Monster überhaupt weiss, wohin es gehen kann, muss es auf Variabeln/Informationen der Klasse Grid/Map zugreifen.

Frage: Schreibe ich nun die Methode Bewegen in der Klasse Grid/Map, weil dort die Informationen über die Position der Wände und der Monster lagern oder schreibe ich die Methode Bewegen in die Klasse Monster, weil Bewegen ja eine Fähigkeit des Monsters ist. Falls ich die in die Klasse Monster schreibe, muss diese Methode also auf Informationen/Variabeln der Klasse Grid/Map zugreifen?
BlackJack

@weitnow: Das kommt darauf an wie Du das genau lösen möchtest und was „bewegen“ für Spielregeln hat, und ob die alleine vom übergeordneten Spiel abhängen oder ob individuelle Monster da auch einen Teil der Regeln kennen die nur für sie gelten. Also soll sich beispielsweise ein Monster von sich aus bewegen können, mit irgendwelchen Entscheidungen die das Monster selber trifft, oder ist das letztendlich komplett ”fremdbestimmt”?
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hi BlackJack

Also ich habe mir das so vorgestellt:

Klasse Monster:
Hat u.a. die eigene Position als Variable

Klasse Grid/Map:
Hat eine Liste mit allen Instanzen der Klasse Monster. Die Klasse Grid/Map benötigt diese Liste, um alle Monster auf dem Grid/Map darzustellen.
Die Klasse Grid wird einmal pro Runde aufgerufen bzw. die Methode print_map der Klasse Grid, welche die aktuelle Position aller Gegestände auf dem Grid auf dem Bildschirm zeigt.

Die Klasse Monster weiss somit eigentlich nur seine eigene Position. Nun möchte ich, dass das Monster eine Variable Bewegungsgeschwindigkeit hat. Das Monster kann sich also pro Runde auf dem Grid ein oder zwei Felder bewegen. Die Methode Bewegen verschiebt in der Folge die Position des Monsters ein oder zwei Felder in eine beliebige Richtung. Allerdings muss ja geprüft werden, ob das Feld, wo es sich hin bewegen möchte, frei ist bzw. keine Wand im Wege steht oder bereits ein anderes Monster auf diesem Feld ist. Die Methode Bewegen soll also Bewegungen gemäss der Bewegungsgeschwindigkeit des Monsters ermöglichen. Das Monster wird einfach per Zufallgenerator in irgend eine Richtung bewegt. Falles es sich beim Monster um den Spieler handelt, dann kann dieser natürlich bestimmen, wohin er sich bewegen möchte. Wenn ich nun die Methode Bewegen der Klasse Monster zuordne, wie kann ich dann sicherstellen, dass sich das Monster nicht in eine Wand bewegt? Denn die Klasse Monster weiss ja nur seine jetzige Position. Nur die Klasse Grid kennt die Position aller Monster und aller Wände. Oder muss sogar eine Funktion integriert werden, mit welcher die Klasse Monster bei der Klasse Grid nachfragen kann, ob dies ein valider Zug/Bewegung ist?

Ach ja, natürlich möchte ich mit der Zeit die "Intelligenz" des Monsters verbessern. Später soll es sich nicht nur zufällig bewegen sondern zum Beispiel, wenn es Hunger hat, nach Essen auf dem Grid suchen oder den Spieler in Sichtweite angreifen etc.

Dann hätte ich gerne Monster, welche sich nur in einem gewissen Abschnitt der Map bewegen können und solche, welche über die ganze Karten wandern können. Dies würde ich dann auch gerne in der Methode Bewegen definieren. Aber wie gesagt, ich weiss nicht, ob ich diese Methode lieber der Klasse Grid oder Monster anhängen soll....ich denke Monster wäre besser, aber wie prüfe ich dann, ob es ein valider Zug ist oder sich das Monster in eine Wand bewegt?

Wie würdest du das lösen BlackJack?

PS: Erneut besten Dank, dass du dir immer Zeit nimmst. Ich schätze alles Tipps die ich jeweils hier im Forum erhalte sehr :D
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hm, ich habe es jetzt so gelöst:

Der Klasse Grid/Map habe ich eine Methode spendiert:

Code: Alles auswählen

def check_if_position_is_free(self, position):
        """ Prueft ob sich an der Position keine Wand befindet und die Position auf dem Grid ist"""
        x, y = position
        try:
            if self.wallist[y][x] == 1: # 1  bedeutet es ist eine Wand
                return False
            else:
                return True
        except IndexError:
            # IndexError bedeutet, Position nicht mehr auf dem Grid bzw. ausserhalb des Grids
                   return False
Der Klasse Monster habe ich eine Methode "move" gegeben.

Die Methode "move" von Monster ruft die Methode "check_if_position_is_free" der Klasse Grid/Map auf. Falls True, dann verschiebt die Methode
move das Monster auf die neue Position.

Keine Ahnung, ob das schlau ist es so zu lösen....Feedback ist sehr willkommen :D
Zuletzt geändert von Anonymous am Freitag 10. Juni 2016, 15:30, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Konkret ist mir die Implementierung zu langatmig - ein einfaches "return walllist[x][y]" statt der kompletten inneren If-Abfrage reicht. Drumrum bleibt alles gleich.

Und architektonisch finde ich den Ansatz, dem Monster Zugriff auf die Karte zu geben durchaus sinnvoll. Ich habe ja auch Zugriff auf die Welt um mich herum, und kann die Frage danach, ob ich vor einer Wand stehe, beantworten.

Im allgemeinen baut man Game-Engines auch so auf, dass die Spielobjekte wie Monster, Spieler etc. in einer Schleife vor der Darstellung der Spielwelt die Gelegenheit bekommen, sich aktualisieren. Als zB eine Methode update() aufzurufen. Ob sich darin bewegt, angegriffen oder sonstewas wird, ist dann von der konkreten Klasse abhaengig. Diese update-Methode kann zB als Argument die Welt/Karte bekommen, so dass eben zB die Waende oder die Spielerposition abgefragt werden koennen.
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

Ein anderer Ansatz wäre, dem Actor (Spieler/Monster) eine Art Intention (move south) zu geben, welche durch die Benutzereingabe oder der KI gesetzt würde.

Die übergeordnete "Physik" prüft, ob diese möglich ist und führt diese aus oder eben nicht ("Ye cannot pass.").

DIE Lösung gibt es hier ohnehin nicht.
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hey Deets, hallo bwbg

Herzlichen Dank für euer Feedback :D

@bwbg:
Ich glaube deine Idee habe ich im Moment umgesetzt. Natürlich seeeeeeehr vereinfacht. Die Klasse Monster hat eine move Methode, welche aufgerufen wird um das Monster auf der Spielwelt zu bewegen. Diese Methode ruft dann eine Methode der Klasse Grid/Map (Spielfeld) auf und prüft ob dies überhaupt ein valider Zug ist (You shall not pass :))).

@Deets:
Oh ja, das hört sich nach einem guten Konzept an. Verstehe ich das richtig, dass die Methode update() in meinem Beispiel also in der Klasse Grid/Map (Spielwelt) angesiedelt wäre. Die Monsterklasse (also ich rede natürlich hier immer von einer Instanz der Klasse) würde dann die externe update() Methode aufrufen und bekommt darin alle Informationen der Spielwelt zurückgeliefert?
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

weitnow hat geschrieben:

Code: Alles auswählen

def check_if_position_is_free(self, position):
        """ Prueft ob sich an der Position keine Wand befindet und die Position auf dem Grid ist"""
        x, y = position
        try:
            if self.wallist[y][x] == 1: # 1  bedeutet es ist eine Wand
                return False
            else:
                return True
        except IndexError:
            # IndexError bedeutet, Position nicht mehr auf dem Grid bzw. ausserhalb des Grids
                   return False
Geht auch kürzer:

Code: Alles auswählen

def is_free(self, position):
    x, y = position
    try:
        return not self.walls[x][y]
    except IndexError
        # Position out of grid
        return False
Anstatt Nullen und Einsen zum Markieren der Wände würde ich übrigens auch besser True und False verwenden. Wobei das zumindest beim händischen Erstellen von Karten natürlich etwas aufwändiger ist.
BlackJack

@weitnow: ”Die” `update()`-Methode wäre bei Monster *und* Game. `Game.update()` bedeutet alle enthaltenen Monster zu aktualisieren und ein `Monster.update()` macht dann was immer es so machen will.

Code: Alles auswählen

class Dungeon(object):

    def update(self):
        for monster in self.monsters:
            monster.update(self)


class Monster(object):

    def update(self, dungeon):
        possible_positions = dungeon.get_free_positions_around(self.position)
        dungeon.move_object(self, random.choice(possible_positions))
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Alles klar, ich glaube nun verstehe ich es. Ich werde es gleich versuchen in meinem kleinen Spiel umzusetzen.

Nochmals vielen Dank an euch alle für die lehrreichen Antworten.

Geniesst das Wochenende :D
Benutzeravatar
weitnow
User
Beiträge: 17
Registriert: Dienstag 8. September 2015, 15:36

Hallo zusammen

Ich wollte euch noch eine Rückmeldung geben. Besten Dank für den Tipp und die Erklärung die update()-Methode in beide Klassen zu implementieren. Ich habe das jetzt so gemacht und bin mit dem Resultat sehr zufrieden. Mit dieser Lösung konnte ich die logisch der Monsterklasse zugehörigen Methoden (bewege dich, prüfe ob Feind in der Nähe ist, prüfe ob du gegen Wand läufst etc) auch in der Monsterklasse deponieren und via Update jeweils die Spielfeldinfos abgleichen. Coole Sache....
Antworten