Klassen, Funktionen - Tipps und Kritik benötigt

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.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Hey Leute,

ich mal wieder...dümpel immer noch als blutiger Anfänger rum und komme zu nichts. Um meine Kenntnisse (vor Allem im Bezug auf OOP) ein wenig zu festigen, habe ich mich an eine Übung gewagt, die sicherlich nicht neu und auch nicht sonderlich kreativ ist.

Kurzer Umriss:

Es handelt sich um ein "Spiel", welches zur Zeit noch nicht interaktiv ist. Es gibt einen Spieler und einen Gegner. Beide greifen sich in jeder Spielrunde gegenseitig an und ziehen sich Lebenspunkte ab. Hat ein Teilnehmer <= 0 Lebenspunkte, dann verliert er und das Spiel ist zuende.

Hier mein Code:

Code: Alles auswählen

#!/usr/bin/env python

class Charakter:
    alive = True
    loser = ""

    def __init__(self, name, life, strength):
        self.name = name
        self.life = life
        self.strength = strength
        
    def get_life(self):
        print("Life: " + str(self.life))
        
    def get_name(self):
        print("Name: " + self.name)
        
    def get_status(self):
        if self.life <= 0:
            Charakter.alive = False
            print("Dead")
            loser = self.name
            print ("Loser: " + loser)
        else:
            print("Alive")

class Enemy(Charakter):

    def __init__(self, name, life, strength, race, xp):
        self.name = name
        self.life = life
        self.strength = strength
        self.race = race
        self.xp = xp
        
    def get_race(self):
        print(self.race)
        
    def attack(self, Player):
        print (self.name + "(" + self.race + ")" + " attacked...")
        print (str(self.strength) + " hp removed...")
        Player.life = Player.life - self.strength
                
class Player(Charakter):
    
    race = "Human"

    def __init__(self, name, life, strength):
        self.name = name
        self.life = life
        self.strength = strength
        
    def get_race(self):
        print(self.race)
        
    def attack(self, Enemy):
        print (self.name + "(" + self.race + ")" + " attacked...")
        print (str(self.strength) + " hp removed...")
        Enemy.life = Enemy.life - self.strength
        
horst = Enemy("Horst", 50, 55,"Alien", 45)
chris = Player("Chris", 100, 15)  
  
round = 1  

while Player.alive == True and Enemy.alive == True:
        
    print ("")
    print ("----------------")
    print ("Round " + str(round))
    print ("----------------")
    print ("")
    round = round + 1

    chris.get_name()
    chris.get_life()
    horst.attack(chris)
    chris.get_name()
    chris.get_life()
    chris.get_status()

    if Player.alive == False or Enemy.alive == False:
        break
    
    horst.get_name()
    horst.get_life()
    chris.attack(horst)
    horst.get_name()
    horst.get_life()
    horst.get_status()

    if Player.alive == False or Enemy.alive == False:
        break
Ich bin mir sicher, dass die attack() Funktion definitiv in die Elternklasse Charakter gehört, da ja Spieler wie Gegner diese nutzen, allerdings liegt mir momentan fern, wie ich dann entscheiden kann, wer wen angreift, beim Aufruf der Funktion.

Was mir auch ABSOLUT nicht gefällt, ist die Überprüfung am Ende des Codes:

Code: Alles auswählen

if Player.alive == False or Enemy.alive == False:
        break
Stirbt der Gegner zuerst, dann beendet das Spiel korrekterweise, aber stirbt der Spieler zuerst, dann greift er trotzdem nochmal an. Das bin ich mit der doppelten Abfrage umgangen. Ich denke der richtige Ansatz wäre aber, in der Funktion get_status() eine Abbruchbedingung einzuführen. Aber ein break funktioniert nicht, da man sich innerhalb der Funktion selbst ja nicht in einer Schleife befindet.

Bin auf eure Vorschläge, Ideen und Kritiken gespannt!

Liebe Grüße

Christopher
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Ohne jetzt im Detail alles betrachtet zu haben:

In jeder Runde läuft offensichtlich 2x fast ein identischer Ablauf durch, der sich nur dadurch unterscheidet auf welche Objekte zugegriffen wird.
Das schreit danach in einer Funktion gekapselt zu werden.

Zum Beispiel so:

Code: Alles auswählen

def play_game_round(active_player, oponent):
    active_player.get_name()
    [...]
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

...und statt:

Code: Alles auswählen

print (str(self.strength) + " hp removed...")
lieber so:

Code: Alles auswählen

print("%s hp removed..." % self.strength)
:wink:

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
MagBen
User
Beiträge: 799
Registriert: Freitag 6. Juni 2014, 05:56
Wohnort: Bremen
Kontaktdaten:

CrisBee hat geschrieben:Ich bin mir sicher, dass die attack() Funktion definitiv in die Elternklasse Charakter gehört, da ja Spieler wie Gegner diese nutzen, allerdings liegt mir momentan fern, wie ich dann entscheiden kann, wer wen angreift, beim Aufruf der Funktion.
Das Problem beginnt mit der Namensgebung. Für den Player ist die KI-Figur ein Enemy. Du bist aber nicht der Player, sondern Du machst das Spiel und für die KI-Figur ist auch der Player ein Enemy. Also such Dir für die Klasse Enemy einen anderen Namen. Wenn Du das gemacht hast, dann kannst Du ohne Verwirrung zu stiften die attack Methode in der Klasse Charakter so schreiben:

Code: Alles auswählen

    def attack(self, enemy):
        enemy.life -= self.strength
Der Angreifer ist immer self und der Angegriffene ist immer enemy, egal ob self ein Charakter oder eine KI-Figur ist.

In Deinen attack Methoden da bringst Du auch Variablennamen und Klassennamen durcheinander. Du schreibst den Variablennamen wie einen Klassennamen, nämlich groß.
a fool with a tool is still a fool, www.magben.de, YouTube
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@CrisBee: hier die Tipps und Kritik:
- alive und looser sind keine Klassenattribute, sollten also in __init__ gesetzt werden
- die get_...-Methoden sind allesamt keine get-Methoden, sie geben etwas aus, sollten also print_life usw. heißen
- get_status ist dagegen eher ein update_and_print_status, es ist wichtig, schon am Namen zu erkennen, ob eine Methode den Zustand des Objekts ändert, oder nur abfrägt. Character.alive muß natürlich self.alive heißen.
- Enemy.__init__ ruft nicht das Parent-init auf, sondern setzt alle Attribute selbst nochmal, das ist nicht nur Code-Verdoppelung, Du mußt auch noch alle Änderungen in Characer.__init__ in allen anderen __init__s mitpflegen, was spätestens Morgen zu Programmierfehlern führt.
- in Enemy.attack sollte Player klein geschrieben werden, um es nicht mit der Klasse Player zu verwechseln.
- Benutze String-Formatierung anstatt dieses Zusammenstückeln von Strings mit +
- Player ist exakt gleich wie Enemy, die Klasse kann also weg.
Zeile 66: die while-Schleife greift auf die Klassen-Attribute zu, die keine mehr sein sollten

Auf Modul-Ebene darf keine ausführbarer Code stehen, d.h. alles ab Zeile 61 gehört in eine Funktion, die üblicherweise main heißt und am Schluß per

Code: Alles auswählen

if __name__ == '__main__':
    main()
gestartet wird.

Ich denke, das reicht für's erste. Wenn Du die groben Fehler alle korrigiert hast, können wir ja nochmal an die Feinheiten ran.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Ich danke euch für die informativen Ratschläge! Vor Allem Sirius3, da hast du dir echt Mühe geben, ich weiß das zu schätzen. Ich versuche das gleich so umzusetzen und melde mich dann mit neuen Code! Bis dahin hoffe ich, dass Alfons sich nicht hier einmischt, sonst ist der nächste Thread dicht... :mrgreen: :roll:
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Aaaaalso, soweit mit den Anpassungen:

Code: Alles auswählen

#!/usr/bin/env python

class Attacker:

    def __init__(self, name, life, strength, xp):
        self.alive = True
        self.loser = ""
        self.name = name
        self.life = life
        self.strength = strength
        self.xp = xp
        
    def print_life(self):
        print("Life: %s" % self.life)
        
    def print_name(self):
        print("Name: %s" % self.name)
        
    def update_print_status(self):
        if self.life <= 0:
            self.alive = False
            print("Dead")
            self.loser = self.name
            print ("Loser: %s" % self.loser)
        else:
            print("Alive")
            
    def attack(self, attacker):
        print ("%s attacked..." % self.name)
        print ("%s hp removed..." % self.strength)
        attacker.life -= self.strength

def main():
    horst = Attacker("Horst", 50, 5, 45)
    chris = Attacker("Chris", 100, 15, 0)  
  
    round = 1  

    while horst.alive == True and chris.alive == True:
        
        print ("")
        print ("----------------")
        print ("Round %s" % round)
        print ("----------------")
        print ("")
        round = round + 1

        chris.print_name()
        chris.print_life()
        horst.attack(chris)
        chris.print_name()
        chris.print_life()
        chris.update_print_status()

        if horst.alive == False or chris.alive == False:
            break
    
        horst.print_name()
        horst.print_life()
        chris.attack(horst)
        horst.print_name()
        horst.print_life()
        horst.update_print_status()

        if horst.alive == False or chris.alive == False:
            break

if __name__ == '__main__':
    main()        
Jetzt habe ich nur noch die Klasse Attacker. Was mich dabei etwas wurmt ist, dass ich vorher - mit Player und Enemy - beim Enemy noch die "xp" hatte und die sollte der Player nicht haben. War so gedacht, dass der Spieler später Erfahrungspunkte für jeden besiegten Gegner bekommt, aber man selbst gibt ja keine an den Gegner ab, wenn man verliert. Jetzt haben alle Objekte xp. Habe dem "Spieler" jetzt ersatzweise 0 xp gegeben! :D
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Wie gesagt, die main-Funktion kannst du noch einmal verkürzen:

Code: Alles auswählen

def play_round(active, oponent):
    active.print_name()
    active.print_life()
    oponent.attack(active)
    active.print_name()
    active.print_life()
    active.update_print_status()
    

 
def main():
    horst = Attacker("Horst", 50, 5, 45)
    chris = Attacker("Chris", 100, 15, 0)  
 
    round = 1  
 
    while horst.alive == True and chris.alive == True:
       
        print ("")
        print ("----------------")
        print ("Round %s" % round)
        print ("----------------")
        print ("")
        round = round + 1
 
        play_round(chris, horst)
 
        if horst.alive == False or chris.alive == False:
            break
   
        play_round(horst, chris)
         
        if horst.alive == False or chris.alive == False:
            break
Wobei play_round noch nicht so ganz als Name passt, denn eigentlich wird ja nur ein Teil der Runde abgebildet, während du eine Runde ja aus zwei Teilen bestehend siehst.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Ach, klar...danke sparrow! Beim Aufruf von "play_round" (ich habe es jetzt fight genannt) einfach die Rollen tauschen, ist ja logisch. Könnte jetzt aber auch innerhalb von fight so:

Code: Alles auswählen

    active.print_name()
    active.print_life()
    opponent.attack(active)
    active.print_name()
    active.print_life()
    active.update_print_status()
    opponent.print_name()
    opponent.print_life()
    active.attack(opponent)
    opponent.print_name()
    opponent.print_life()
    opponent.update_print_status()
schreiben und dann doch quasi eine Runde daraus machen und diese jeweils nur einmal in der Main() aufrufen. Allerdings kommt dann wieder die Problematik zu Stande, dass ein toter Kämpfer dennoch nochmal angreifen kann. Behaupte ich jetzt einfach mal ungetestet! :D
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Attacker() passt auch nicht so ganz, weil er ja auch ein Angegriffener ist. Vielleicht passt besser sowas wie People() ?

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Dann ändere ich den Klassennamen in Participant!^^
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Bauste jetzt ein Nethack-Klon? Würde ich gut finden ;)
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Das war eigentlich nicht mein Ziel! :D Dachte eher an so Zufallskämpfe wie in allen gängigen RPGs. Habe ja auch wiedermal ohne große Vorplanung angefangen. Keine Klassendiagramme, keine Struktogramme, nichts...dabei sollte ich DAS eigtl. auch lernen... -.-
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

CrisBee hat geschrieben:

Code: Alles auswählen

class Charakter:
    alive = True
    loser = ""

    [...]
Welchen Zweck sollen denn die Klassenattribute hier haben? Als Attribute der jeweiligen Instanz sind sie deutlich besser aufgehoben.

Code: Alles auswählen

class Character:
    alive = True

    def kill(self):
        Character.alive = False


class Player(Character):
    pass


class Enemy(Character):
    pass

player = Player()
enemy = Enemy()
enemy.kill()
print(player.alive)
Und - oh Überraschung! - das Ergebnis ist False.
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Das Ganze soll ja später durch einen menschlichen Spieler beeinflussbar sein. Heißt es müsste bei der Klasse Participant noch ein Attribut - nennen wir es human - geben und je nachdem True oder False sein.
Dann eine Funktion namens move() (Spielzug), darin die Routine was der Spieler machen kann: Angreifen, Item verwenden, Fliehen. Ist der Participant menschlich, dann per Eingaben, ist er nicht-menschlich, dann automatisiert.

Bin ich da gedanklich auf dem richtigen Weg?

Ich überlege auch, ob es nicht Sinn macht, eine Klasse Game einzurichten?

Mein aktueller Code:

Code: Alles auswählen

#!/usr/bin/env python

class Participant:

    def __init__(self, name, life, strength, xp):
        self.alive = True
        self.loser = ""
        self.name = name
        self.life = life
        self.strength = strength
        self.xp = xp
        
    def print_life(self):
        print("Life: %s" % self.life)
        
    def print_name(self):
        print("Name: %s" % self.name)
        
    def update_print_status(self):
        if self.life <= 0:
            self.alive = False
            print("Dead")
            self.loser = self.name
            print ("Loser: %s" % self.loser)
        else:
            print("Alive")
            
    def attack(self, attacker):
        print ("%s attacked..." % self.name)
        print ("%s hp removed..." % self.strength)
        attacker.life -= self.strength

def fight(active, opponent):
    active.print_name()
    active.print_life()
    opponent.attack(active)
    active.print_name()
    active.print_life()
    active.update_print_status()

def main():
    horst = Participant("Horst", 50, 5, 45)
    chris = Participant("Chris", 100, 15, 0)  
  
    round = 1  

    while horst.alive == True and chris.alive == True:
        
        print ("")
        print ("----------------")
        print ("Round %s" % round)
        print ("----------------")
        print ("")
        round = round + 1

        fight(chris, horst)

        if horst.alive == False or chris.alive == False:
            break
    
        fight(horst, chris)

        if horst.alive == False or chris.alive == False:
            break

if __name__ == '__main__':
    main()
Danke für eure Geduld! :)
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
MagBen
User
Beiträge: 799
Registriert: Freitag 6. Juni 2014, 05:56
Wohnort: Bremen
Kontaktdaten:

CrisBee hat geschrieben:Das Ganze soll ja später durch einen menschlichen Spieler beeinflussbar sein. Heißt es müsste bei der Klasse Participant noch ein Attribut - nennen wir es human - geben und je nachdem True oder False sein.
Das Attribut würde ich weglassen. Dein erster Ansatz mit verschiedenen Klassen für Spieler- und für KI-Figuren mit einer gemeinsamen Basisklasse halte ich für besser. Du hast sonst sehr schnell in vielen Methoden viele if-Abfragen nach diesem Attribut, d.h. Du presst zwei Implementation in eine Methode. Irgenwann soll dann auch die Spieler-Figur auf Spieler-Signale reagieren und die KI-Figur KI bekommen, spätestens dann brauchst Du verschiedene Klassen.
CrisBee hat geschrieben:Ich überlege auch, ob es nicht Sinn macht, eine Klasse Game einzurichten?
Unbedingt, irgendwo muss die Endlosschleife, die das Spiel antreibt ja rein. Eine andere Möglichkeit ist, dass Du die Endlosschleife von einem GUI-Toolkit benutzt (Event-Thread), aber auch dann brauchst Du sowas wie eine Klasse Scene, die die Figuren, Gegenstände, Gänge, Pfade ... verwaltet.

Geht es Dir nur ums OO-Lernen oder willst Du etwas Spielbares realisieren?
Wenn Du etwas wirklich Spielbares realisieren willst, dann würde ich Dir empfehlen in einer vorhandenen Game-Engine (z.B. Blender) Dein Spiel in Python zu realisieren und nicht auch noch die Game-Engine selbst zu entwickeln.
a fool with a tool is still a fool, www.magben.de, YouTube
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Es geht mir um Beides MagBen, wobei das OOP Lernen im Vordergrund steht. Erstmal! ;-)
Ich kenne Blenders Game Engine, aber würde ich mich direkt da versuchen reinzufuchsen, dann geht fehlen mir wahrscheinlich wieder die Basics.

Freut mich, dass mein erster Ansatz scheinbar doch gut war, ich model das wieder um. Danke! :)
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Verzeiht mir den Doppelpost, aber es hat keiner mehr was geschrieben und ich habe Angst, dass der Thread bei einem EDIT untergeht!^^

Habe eure Kritik und Vorschläge beherzigt - hoffe ich zumindest :P - und den Code nochmal frisch aufgezogen. So sieht es jetzt aus:

Code: Alles auswählen

#!/usr/bin/env python

class Game:
    def __init__(self, message):
        self.message = message

    def game_loop(self, player, enemy):
        while player.health > 0 and enemy.health > 0:
            print("%s %s %s %s" % (player.get_name(), player.get_health(), player.get_strength(), player.get_xp()))
            print("%s %s %s %s" % (enemy.get_name(), enemy.get_health(), enemy.get_strength(), enemy.get_xp()))
            
            self.fight(player, enemy)
            self.fight(enemy, player)
            
        if player.health <= 0:
            print("You lose!")
        else:
            print("You win!")
            
    def fight(self, active, opponent):
        opponent.attack(active)

class Character:
    def __init__(self, name, health, strength, xp):
        self.name = name
        self.health = health
        self.strength = strength
        self.xp = xp

    def get_name(self):
        return self.name

    def get_health(self):
        return self.health

    def get_strength(self):
        return self.strength
        
    def get_xp(self):
        return self.xp
        
    def attack(self, opponent):
        opponent.health -= self.strength

class Player(Character):
    pass
     
class Enemy(Character):
    pass

def main():
    christopher = Player("Christopher", 100, 15, 10)
    guenther = Enemy("Guenther", 50, 25, 5)
    game = Game("Running")
    game.game_loop(christopher, guenther)

if __name__ == "__main__":
    main()
Was mir gefällt ist, dass der Code immer weniger Zeilen bekommt, dabei aber scheinbar gleiches macht. Echt super!^^
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
BlackJack

@CrisBee: Es werden noch weniger Zeilen wenn Du die sinnlosen `get_*()`-Methoden einfach rauswirfst. :-)
Benutzeravatar
CrisBee
User
Beiträge: 61
Registriert: Mittwoch 2. Oktober 2013, 10:45
Wohnort: Bielefeld
Kontaktdaten:

Meinst du so, BlackJack? :mrgreen:

Code: Alles auswählen

#!/usr/bin/env python

class Game:
    def __init__(self, message):
        self.message = message

    def game_loop(self, player, enemy):
        while player.health > 0 and enemy.health > 0:
            print("%s %s %s %s" % (player.name, player.health, player.strength, player.xp))
            print("%s %s %s %s" % (enemy.name, enemy.health, enemy.strength, enemy.xp))
            
            self.fight(player, enemy)
            self.fight(enemy, player)
            
        if player.health <= 0:
            print("You lose!")
        else:
            print("You win!")
            
    def fight(self, active, opponent):
        opponent.attack(active)

class Character:
    def __init__(self, name, health, strength, xp):
        self.name = name
        self.health = health
        self.strength = strength
        self.xp = xp
        
    def attack(self, opponent):
        opponent.health -= self.strength

class Player(Character):
    pass
     
class Enemy(Character):
    pass

def main():
    christopher = Player("Christopher", 100, 15, 10)
    guenther = Enemy("Guenther", 50, 25, 5)
    game = Game("Running")
    game.game_loop(christopher, guenther)

if __name__ == "__main__":
    main()
Das Reallife ist nur etwas für Leute, die keine Freunde im Internet haben! :P
Meine Fotografie: http://www.cutefeet.de
Antworten