Tic Tac Toe von Domroon

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Freitag 30. April 2021, 13:13

Hallo Leute,

ich habe ein Tic Tac Toe - Spiel für die Kommandozeile programmiert. Dabei habe ich versucht so viele Tipps (welche ich durch das Forum mitnehmen konnte) wie möglich umzusetzen. Ich weiß nicht ob ich alles so gemacht habe "wie man es tun sollte". Für entsprechende Rückmeldungen wäre ich diesbezüglich sehr dankbar.
Zunächst möchte ich einige Punkte aufzählen welche mir selbst nicht so gut gefallen (auch hier bin ich über jegliche Rückmeldung dankbar). Anschließend poste ich alle Klassen und die main-Funktion.

a)
In "Matchfield.determine_winner()" gefallen mir die vielen If-Abfragen nicht. Auch der counter im else-Zweig macht mich innerlich unruhig. Ich weiß nicht wie ich es "schöner" hinbekommen soll. Vielleicht hat einer von euch eine bessere Idee.

b)
Zu "Matchfield.print()": Ja ich weiß, "Variablen setzt man nicht, man initialisiert sie". Ich habe keine Idee wie ich das setzen eines leeren Strings vermeiden kann :(

c)
Es ist in der main-Funktion natürlich quatsch nach jedem Zug abzufragen ob es einen Gewinner gibt (zumal jedes mal diese vielen If-Abfragen ausgeführt werden => sehr uneffizient). Hier wäre ich über eine "pythonische" Lösung sehr dankbar ;)

Hier mein Code:

Game-Klasse:

Code: Alles auswählen

class Game:
    def __init__(self):
        self.matchfield = Matchfield()
        self.player_1 = Player(input("Player 1, please choose a sign: "), "Player 1")
        self.player_2 = Player(self.get_sign(), "Player 2")

    def get_sign(self):
        if self.player_1.sign == "X":
            return "O"
        else:
            return "X"

    def make_move(self, player):
        while True:
            try:
                player_pos = int(input(f'{player.name}, please name a number (1-9): ')) - 1
                player.add_sign(self.matchfield.choices, player_pos)
                self.matchfield.print()
                break
            except ValueError:
                print("Wrong Input. Please try again.")
            except IndexError:
                print("Please name a number betwenn 0-8")
        
    def get_winner(self):
        if self.matchfield.determine_winner() == "X":
            print("Player with sign 'X' win!")
            return True
        elif self.matchfield.determine_winner() == "O":
            print("Player with sign 'O' win!")
            return True
        elif self.matchfield.determine_winner() == "Nobody":
            print("Nobody wins!")
            return True
        else:
            return False
Matchfield-Klasse:

Code: Alles auswählen

class Matchfield:
    def __init__(self):
        self.choices = ['-', '-', '-', '-', '-', '-', '-', '-', '-']

    def determine_winner(self):
        if self.choices[0] == self.choices[1] == self.choices[2]:
            return self.choices[0]
        elif self.choices[3] == self.choices[4] == self.choices[5]:
            return self.choices[3]
        elif self.choices[6] == self.choices[7] == self.choices[8]:
            return self.choices[6]
        elif self.choices[0] == self.choices[3] == self.choices[6]:
            return self.choices[0]
        elif self.choices[1] == self.choices[4] == self.choices[7]:
            return self.choices[1]
        elif self.choices[2] == self.choices[5] == self.choices[8]:
            return self.choices[2]
        elif self.choices[0] == self.choices[4] == self.choices[8]:
            return self.choices[0]
        elif self.choices[2] == self.choices[4] == self.choices[6]:
            return self.choices[2]  
        else:
            counter = 0
            for sign in self.choices:
                if sign != "-":
                    counter += 1
                    if counter == 9:
                        return "Nobody"

    def print(self):
        output_string = ""
        for choice in range(0, len(self.choices)):
            output_string += "|" + self.choices[choice]
            if (choice + 1) % 3 == 0:
                output_string += "|\n"
        print(output_string)
Player-Klasse:

Code: Alles auswählen

class Player:
    def __init__(self, sign, name):
        self.sign = sign
        self.name = name

    @property
    def sign(self):
        return self._sign

    @sign.setter
    def sign(self, sign):
        if not sign in ["X", "O"]:
            raise ValueError("Attribute must be 'X' or 'O'")
        self._sign = sign 

    def add_sign(self, choices, position):
        if choices[position] not in "-":
            raise ValueError("Position ist not empty")
        choices[position] = self.sign
Main-Funktion:

Code: Alles auswählen

def main():
    while True:
        try:
            game = Game()
            break
        except ValueError:
            print("Wrong Input. Please try again.")

    print(f'{game.player_1.name} sign: {game.player_1.sign}')
    print(f'{game.player_2.name} sign: {game.player_2.sign}\n')
    game.matchfield.print()

    while True:
        game.make_move(game.player_1)
        if game.get_winner():
            break
        game.make_move(game.player_2)
        if game.get_winner():
            break
jerch
User
Beiträge: 1664
Registriert: Mittwoch 4. März 2009, 14:19

Freitag 30. April 2021, 16:28

Ein paar Anregungen von meiner Seite:

zu a und c)

Wenn Du den internen Feldzustand mit einem anderen Datentypen ausdrückst, kannst Du auf Pythoninterna zurückgreifen und dadurch kompakteren Code schreiben. Zunächst erst einmal sind die vielen if-Konditionen grundsätzlich nötig, da sie ja die Gewinnregeln überprüfen sollen. Die könnte man aber auch anders ausdrücken, z.B. in einem Container, der alle Gewinnregeln abstrahiert. Wenn Du jetzt noch den Feldzustand clever wählst, kannst Du sowohl die Überprüfung der Gewinnregeln als auch des Endzustandes einkürzen (alles ungetestet):

Code: Alles auswählen

STATES = {0: '-', 1: 'x', -1: 'o'}
WIN_INDICES = [
    [0, 1, 2], [3, 4, 5], [6, 7, 8],  # horizontal
    [0, 3, 6], [1, 4, 7], [2, 5, 8],  # vertical
    [0, 4, 8], [2, 4, 6]              # diagonal
]

class Matchfield:
    def __init__(self):
        self.choices = [0, 0, 0, 0, 0, 0, 0, 0, 0]
    def determine_winner(self):
        for indices in WIN_INDICES:
            # win condition - fields add up to min or max
            if sum(self.choices[idx] for idx in indices) in [3, -3]:
                return self.choices[indices[0]]
        if all(self.choices):
            return 'Nobody'
Natürlich musst Du dann eine Rückübersetzung des Status überall dort machen, wo Du an das Zeichen ranmöchtest:

Code: Alles auswählen

# old
do_something_with_XO(self.choice[xy])
# change to
do_something_with_XO(STATES[self.choice[xy]])
Des Weiteren gibt es noch ein paar Optimierungsmöglichkeiten. So ist z.B. der Test aller Gewinnregeln nicht nötig, da ein gerade gesetztes Feld in max. 3 Regeln vorkommt. Weiterhin müssen die ersten beiden Züge nicht gegen die Gewinnregeln geprüft werden, da alle Regeln erst nach 3 Zügen greifen. Hier wäre die Überlegung sinnvoll, ob man nicht die Züge zusätzlich am Board mitspeichert.
Für Tic-Tac-Toe sind solche Optimierungen eher nebensächlich (sehr kleiner Suchraum), für größere Boardgames können diese aber wesentlich für einen Geschwindigkeitgewinn einer AI werden. Falls eine AI nicht zuviel des Guten für Dich ist, such mal nach alpha-beta pruning und NegaMax, ist für Tic-Tac-Toe sehr einfach umsetzbar.

zu b)

Die Boardausgabe lässt sich etwas kompakter so ausdrücken:

Code: Alles auswählen

    def print(self):
        return f'|{"|".join(line)}|' for line in (self.choices[i*3:i*3+3] for i in range(3)))

# or with integer in choices
     def print(self):
        return f'|{"|".join(STATES[state] for state in line)}|' for line in (self.choices[i*3:i*3+3] for i in range(3)))
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Samstag 1. Mai 2021, 09:55

@jerch:
Vielen Dank Dir! Deine Tipps sind super, ich werde das ganze so schnell wie möglich umsetzen. Auch der alpha-beta pruning Algorithmus hört sich sehr interessant an. Mal schauen ob ich das hinbekommen ;)
Ich melde mich sobald ich etwas an meinem Programm verbessert habe ;)
jerch
User
Beiträge: 1664
Registriert: Mittwoch 4. März 2009, 14:19

Samstag 1. Mai 2021, 11:33

Grad gesehen - der Code für print ist irgendwie kaputt gegangen. Leider kann ich es oben nicht mehr ändern, daher hier nochmal:

Code: Alles auswählen

def print(self):
...     return '\n'.join(f'|{"|".join(line)}|' for line in (self.choinces[i*3:i*3+3] for i in range(3)))
Sirius3
User
Beiträge: 14428
Registriert: Sonntag 21. Oktober 2012, 17:20

Samstag 1. Mai 2021, 12:12

Die Aufteilung der Klassen ist etwas seltsam. Player bekommt Interna der Matchfield-Klasse um dann sich selbst zu setzen.
Für mich ist es auch etwas over-engineered, dass Player prüft, was ein gültiges sign ist.
Du hast kopierten Code für player_1 und player_2. Wenn man anfängt, Variablen durchzunummerieren, möchte man eigentlich eine Liste verwenden.
get_winner ist ein irreführender Name, weil es gar nicht den Gewinner ermittelt, sondern nur, ob es einen gibt.
Und Matchfield und Game könnte man in eine Klasse zusammenfassen.
`not in` bei einem einbuchstabigen String zu verwenden ist falsch, weil Du ja eigentlich den ganzen String vergleichen willst.

Code: Alles auswählen

from itertools import cycle

class Player:
    def __init__(self, sign, name):
        self.sign = sign
        self.name = name

    def __str__(self):
        return f"{self.name} Sign: {self.sign}"

class Game:
    SIGNS = ["X", "O"]
    WIN_INDICES = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8],  # horizontal
        [0, 3, 6], [1, 4, 7], [2, 5, 8],  # vertical
        [0, 4, 8], [2, 4, 6]              # diagonal
    ]

    def __init__(self):
        self.field = ['-'] * 9
        self.players = [
            Player(sign, f"Player {index}")
            for index, sign in enumerate(self.SIGNS, 1)
        ]

    def play(self):
        for player in self.players:
            print(player)
        print()
        self.print_field()
        for player in cycle(self.players):
            self.make_move(player)
            self.print_field()
            winner = self.determine_winner()
            if winner:
                break
        print(f"Winner is {winner}")

    def make_move(self, player):
        while True:
            try:
                player_pos = int(input(f'{player.name}, please name a number (1-9): ')) - 1
                self.add_sign(player, player_pos)
                break
            except ValueError:
                print("Wrong Input. Please try again.")
            except IndexError:
                print("Please name a number betwenn 0-8")

    def determine_winner(self):
        for indices in self.WIN_INDICES:
            choices = set(self.field[i] for i in indices)
            if len(choices) == 1 and choices != {'-'}:
                return choices.pop()
        if not any(c == '-' for c in self.field):
            return 'Nobody'

    def print_field(self):
        for i in range(0, len(self.field), 3):
            print(f"|{'|'.join(self.field[i:i+3])}|")

    def add_sign(self, player, position):
        if self.field[position] != "-":
            raise ValueError("Position ist not empty")
        self.field[position] = player.sign


def main():
    game = Game()
    game.play()
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Sonntag 2. Mai 2021, 08:10

@Sirius3:
Auch Dir vielen Dank ;)
Melde mich sobald ich die Anpassungen vorgenommen habe.
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Montag 3. Mai 2021, 08:58

Hi Leute,
folgende Anpassungen habe ich also vorgenommen (somit entspricht es jetzt genau dem Code von Sirius3):
- WIN_INDICES hinzugefügt
- Player Klasse simpler gestaltet
- init Methode der Game Klasse verbessert
- play Methode hinzugefügt
- make_move Methode verbessert
- get_winner Methode gelöscht
- determine_winner methode verbessert
- print_field methode hinzugefügt
- add_sign methode verbessert
- Matchfield Klasse gelöscht
- main Funktion entsprechend angepasst

Eine Sache: bei der Game.determine_winner() Methode verknotet sich mein Hirn :D Wenn mir jemand diese Methode erklären würde wäre ich diesem sehr dankbar ;)
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Montag 3. Mai 2021, 11:09

@Sirius3: Ich habe mir bei diesem Programm echt den Kopf zerbrochen und mir bei der Erstellung der Klassen echt schwer getan. Hast Du eventuell ein paar einfache Tipps wie ich in Zukunft Klassen effektiv erstellen kann und wie ich mein Programm planen sollte? Denn wenn ich sehe wie "verwurschtelt" mein Programm ursprünglich war und wie klar strukturiert es nun durch euch ist, bin ich echt am verzweifeln :D
jerch
User
Beiträge: 1664
Registriert: Mittwoch 4. März 2009, 14:19

Montag 3. Mai 2021, 11:13

Domroon hat geschrieben:
Montag 3. Mai 2021, 08:58
Eine Sache: bei der Game.determine_winner() Methode verknotet sich mein Hirn :D Wenn mir jemand diese Methode erklären würde wäre ich diesem sehr dankbar ;)
Versuch es doch erstmal selber - kommentiere zunächst zeilenweise, was da passiert und versuche dann, Dir das Verhalten daraus zu erschliessen. Wenns wo hakt, kommentiere das entsprechend, dann können wir das nochmal durchgehen.

Fremdcode lesen und interpretieren zu können, ist extrem wichtig. Da Python relativ ausdrucksstark ist, kann das anfangs schwierig sein. Trotzdem kann man nicht früh genug damit anfangen.
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Montag 3. Mai 2021, 11:32

jerch hat geschrieben:
Montag 3. Mai 2021, 11:13
Fremdcode lesen und interpretieren zu können, ist extrem wichtig. Da Python relativ ausdrucksstark ist, kann das anfangs schwierig sein. Trotzdem kann man nicht früh genug damit anfangen.
Da hast Du recht ;)
Ich denke ich habs nun verstanden. Mit dem Python-Interpreter im Interaktiven Modus "herumzuspielen" kann Wunder wirken.

Code: Alles auswählen

def determine_winner(self):
        for indices in self.WIN_INDICES:			
            choices = set(self.field[i] for i in indices)
            if len(choices) == 1 and choices != {'-'}:
                return choices.pop()
        if not any(c=='-' for c in self.field):
            return 'Nobody'
Vor allem die Zeile wo die Variable 'choices' gesetzt wird hat mir Kopfschmerzen bereitet:
- 'indices' ist eine Liste mit drei Nummern z.B. [0,1,2]
- diese indices werden dazu benutzt um aus der self.field-Liste die Zeichen an den entprechenden Zeilen "heraus zu ziehen"
- weil das ganze in ein Set "verpackt" wird so wird beispielsweise (falls an den Positionen 0, 1 und 2 ausschließlich 'X' steht) nur 'X' in choices gespeichert (denn Duplikate gibt es in einem Set nicht)
- falls an einer dieser drei Position noch ein 'O' oder '-' gespeichert ist, so wird z.B. ein 'X' und ein 'O' in dieses Set gespeichert
- die Nachfolgende if-Abfrage schaut ob nur ein Zeichen in 'choices' abgespeichert ist und ob dieses eine Zeichen nicht '-' ist, Wenn dies der Fall ist so muss an all diesen Positionen 'X' oder 'O' was dann mit "return choices.pop()" gleichzeitig zurückgegeben und gelöscht wird
- mit der if-Abfrage danach wird geschaut ob überall im 'self.field' kein '-' mehr vorhanden ist. Ist dies der Fall so muss das Feld mit lauter 'X' und 'O' gefüllt sein und es gibt somit keinen Gewinner

Genial gelöst! Wäre ich selber aber nie drauf gekommen es so zu lösen. Ich hoffe, dass ich irgendwann auch mal an diesem Punkt komme wo ich es auch schaffe auf diese Art und Weise zu denken.
Benutzeravatar
__blackjack__
User
Beiträge: 8573
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Montag 3. Mai 2021, 12:14

Den Test nach der Schleife könnte man mit ``in`` beziehungsweise ``not in`` noch ein bisschen einfacher ausdrücken:

Code: Alles auswählen

        if not any(c == "-" for c in self.field):
            return "Nobody"
        
        # =>
        
        if "-" not in self.field:
            return "Nobody"
“Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”
jerch
User
Beiträge: 1664
Registriert: Mittwoch 4. März 2009, 14:19

Montag 3. Mai 2021, 13:37

Und wenn Du das jetzt nochmal mit Deinem Originalcode vergleichst, fällt Dir evtl. auf, dass der Kompaktere eigentlich nur eine Umformung ist:

Code: Alles auswählen

        if self.choices[0] == self.choices[1] == self.choices[2]:  # Bug? ist für 3x '-' auch wahr
            return self.choices[0]
        elif self.choices[3] == self.choices[4] == self.choices[5]:
            return self.choices[3]
        ...
 
# herausziehen der Feldpositionen:
         choices = set(self.field[i] for i in [0, 1, 2])
         if len(choices) == 1 and choices != {'-'}:  # testet auch auf nicht '-' (vermeidet Bug von oben)
             return choices.pop()
         choices = set(self.field[i] for i in [3, 4, 5])
         if len(choices) == 1 and choices != {'-'}:
             return choices.pop()
        ...

# Schleife über Positionen:
        WIN_INDICIES = [[0, 1, 2], ...]  # enthält jetzt alle expliziten Gewinnpositionen
        for indices in WIN_INDICES:			
            choices = set(self.field[i] for i in indices)
            if len(choices) == 1 and choices != {'-'}:
                return choices.pop()
        ...
Der untere Abschluss (falls noch kein Gewinner gefunden wurde) macht bei Dir mehr als nötig. So ist z.B. der Counter überflüssig, den Du benutzt um zu entscheiden, ob das Spiel bereits beendet ist. Anstatt alle besetzten Felder zu zählen, kannst Du das aber bereits mit dem ersten Auftreten eines '-' Feldes entscheiden und aussteigen (siehe .__blackjack__'s Schnipsel).
Und Deine `if counter == 9:` Abfrage ist sehr tief geschachtelt, die kannst Du nachrangig evaluieren, da 9 ja nur wirklich am Ende aller Felder auftreten kann. So tief verschachtelte if-Konditionen ohne explizite else-Behandlung sind übrigens eine beliebte Quelle für späteres Fehlverhalten, da man den "Ergebnisraum" aufbläst und schnell eine nötige Zustandsbehandlung übersieht. Wenn unnötig, dann besser vermeiden.
Benutzeravatar
Domroon
User
Beiträge: 66
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Montag 3. Mai 2021, 13:51

@__blackjack__,
@jerch,
@Sirius3:

Vielen Dank. Ich werde schauen, dass ich alles was ich von euch mitnehmen kann in meinem Hirn abspeicher. Eure Tipps und Ratschläge sind echt super ;)
Antworten