@Caskuda: Warum ist denn der Sternchen-Import wieder da? Wird der für irgendetwas tatsächlich verwendet?
Der Inhalt von `TicTacToe.__init__()` und `TicTacToe.reset()` ist identisch. Der sollte nicht zweimal im Quelltext stehen. Methoden die einen Objektzustand wieder in den Ausgangszustand versetzen sind ein potentielle Fehlerquelle wenn die nicht auch als einziges in der `__init__()` aufgerufen werden, weil man bei Veränderungen wie der Zustand verwaltet wird, auch immer daran denken muss die `reset()`-Methode entsprechend anzupassen. Es ist in der Regel einfacher und sicherer einfach ein sauberes, neues Objekt zu erstellen, statt ein vorhandenes wieder zurückzusetzen. Das machst Du ja anscheinend sogar, denn die `TicTacToe.reset()`-Methode wird offenbar nirgends aufgerufen.
Statt der `TicTacToe.show()`-Methode oder zumindest einen Teil davon, würde ich persönlich in eine `__str__()`-Methode verschieben. Denn hier geht es ja um die Zeichenkettendarstellung eines `TicTacToe`-Objekts. Ist natürlich Geschmackssache.
`get_input()` ist keine Methode sondern einfach nur eine Funktion die in die Klasse gesteckt wurde. Das ``continue`` in der Funktion ist sinnlos. Ohne würde genau der gleiche Programmfluss entstehen.
Schräge APIs: `TicTacToe.place()` gibt entweder -1 oder implizit `None` zurück, und `TicTacToe.check()` gibt "player1", "player2", oder implizit `None` zurück. Das sollte nicht sein. Also erst einmal das entweder explizit etwas zurückgegeben wird oder implizit `None` — das sollte alles immer explizit sein, auch ein `None` wenn das ein gültiger Rückgabewert sein kann. Aber es sollte auch keine komischen besonderen Rückgabewerte wie -1 geben. Genau dafür wurden Ausnahmen erfunden.
Bei `TicTacToe.place()` sollte auch keine Benutzerinteraktion erfolgen, also kein `print()`. In einer GUI-Anwendung bekäme das dann ja auch niemand mit, beziehungsweise nur auf einer Konsole wenn es von einer gestartet wurde.
In der `TicTacToeGui.place()` macht ein Rückgabewert gar keinen Sinn, weil die GUI-Hauptschleife den komplett ignoriert.
Ich würde `x` und `y` in `TicTacToe.place()` auch in `row_index` und `column_index` umbenennen, oder die Reihenfolge der Argumente umkehren, denn `y`, `x` ist eine unerwartete Reihenfolge für den Leser.
In die Methode gehört IMHO auch das hochzählen der Spielzugnummer, statt das irgendwo von ausserhalb zu machen und einen inkonsistenten Zustand zu riskieren. Und auch das weitersetzen des aktuellen Spielers kann man dort unterbringen.
Die Spielzugnummer in `TicTacToe.play_game()` am Anfang auf 0 zu setzen ist ein Fehler. Das funktioniert nur wenn das Spielfeld leer ist. Ansonsten kann das ``while`` zu einer Endlosschleife werden.
Ein weiterer Fehler ist die Ausgabe das es keinen Gewinner gibt nach der ``while``-Schleife, denn das wird ja *immer* ausgegeben. Auch dann wenn es einen Gewinner gab und die Schleife mit ``break`` verlassen wurde. Genau für solche Fälle gibt es ``else`` für Schleifen.
`Result` ist eine einfache Funktion als Klasse geschrieben. Das muss man sich auch nicht unbedingt selbst schreiben, `tkinter.messagebox` hat da schon Funktionen die man nutzen könnte.
`GameFrame.__init__()` macht gar nichts mit dem `master`-Argument. Das sollte an `Frame.__init__()` weitergeleitet werden, sonst wird da automagisch das Hauptfenster für genommen. Das ist hier zufällig richtig, aber muss es ja nicht.
Selbst sollte man das nicht verwenden, denn `GameFrame` sollte möglichst nichts über das übergeordnete Widget wissen und da auch keine Methoden über `self.master` aufrufen. Das Objekt ist für die Darstellung des `TicTacToe`-Objekts verantwortlich, sollte darauf dann auch direkt Zugriff haben in dem es übergeben wird. Von dem Objekt sollten dann auch Zeilen- und Spaltenanzahl abgefragt werden, statt fest von 3 auszugehen.
Anstelle einer `GameFrame.reset()`-Methode würde ich eine `update_display()`-Methode schreiben, die das Spielobjekt benutzt um die Schaltflächen zu konfigurieren.
Die Attribute bei `TicTacToeGui` sind irreführend benannt, also mindestens `turn` und `_turn`. Das eine steht für ein `Label`, das andere für eine Zahl.
Auch bei dieser Klasse würde eine `update_display()`-Methode Sinn machen um die Wiederholungen aus dem Code zu bekommen.
Die `reset()`-Methode wäre besser eine `start_new_game()`-Methode ohne die Verkündigung eines Gewinners, dann könnte man die nämlich auch ganz am Anfang einmal aufrufen und kann damit gleichen Code aus der `__init__()` entfernen.
Die `place()`-Methode hat mit dem Platzieren nichts mehr zu tun wenn das im `GameFrame` passiert, sondern ist nur noch für das Prüfen der Endbedingung zuständig. Und da `GameFrame` nichts von der übergeordneten Klasse in der Widgethierarchie wissen sollte, löst man das über eine Rückruffunktion.
Ungetestet:
Code: Alles auswählen
#!/usr/bin/env python3
import tkinter as tk
from functools import partial
from tkinter.messagebox import showinfo
def get_input(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("Wrong value!")
class TicTacToe:
def __init__(self):
self.current_player = 1
self.turn = 1
self.grid = [["_" for _ in range(3)] for _ in range(3)]
def __str__(self):
return "\n".join(" ".join(row) for row in self.grid)
@property
def row_count(self):
return len(self.grid)
@property
def column_count(self):
return len(self.grid[0])
@property
def is_full(self):
return self.turn > self.row_count * self.column_count
def next_player(self):
self.current_player = (self.current_player % 2) + 1
def place(self, row_index, column_index):
if not 0 <= column_index < self.column_count:
raise ValueError("x position is outside of the board!")
if not 0 <= row_index < self.row_count:
raise ValueError("y position is outside of the board!")
if self.grid[row_index][column_index] != "_":
raise ValueError("position is already occupied!")
self.grid[row_index][column_index] = str(self.current_player)
self.turn += 1
self.next_player()
def check(self):
"""
Erinnerung an mich: sau hässlich! je nach Position reichen
2-4 checks.
Bei Vier-Gewinnt oder 5-in-einer-Reihe dringend überarbeiten!
"""
to_check = set()
for row in self.grid:
to_check.add("".join(row))
for column in zip(*self.grid):
to_check.add("".join(column))
to_check.add(
"".join([self.grid[0][0], self.grid[1][1], self.grid[2][2]])
)
to_check.add(
"".join([self.grid[0][2], self.grid[1][1], self.grid[2][0]])
)
for player_number in [1, 2]:
if str(player_number) * 3 in to_check:
return player_number
return None
def play_game(self):
while not self.is_full:
print(f"Board:\n\n{self}\n")
print(f"its the turn of player {self.current_player}")
row_index = get_input("please select a row (0, 1, 2): ")
column_index = get_input("please select a column (0, 1, 2): ")
try:
self.place(row_index, column_index)
except ValueError as error:
print(f"{error} choose again.")
else:
winner = self.check()
if winner is not None:
print(f"The winner is: Player {winner}\n\n{self}\n")
break
else:
print(f"there is no winner!\n\n{self}\n")
class BoardUI(tk.Frame):
def __init__(self, master, tictactoe, turn_callback):
tk.Frame.__init__(self, master)
self._tictactoe = None
self.turn_callback = turn_callback
self.buttons = list()
for row_index in range(tictactoe.row_count):
row = list()
for column_index in range(tictactoe.column_count):
button = tk.Button(
self,
fg="BLACK",
text="-",
padx=5,
pady=5,
command=partial(self.place, row_index, column_index),
)
button.grid(row=row_index, column=column_index)
row.append(button)
self.buttons.append(row)
self.tictactoe = tictactoe
@property
def row_count(self):
return len(self.buttons)
@property
def column_count(self):
return len(self.buttons[0])
@property
def tictactoe(self):
return self._tictactoe
@tictactoe.setter
def tictactoe(self, value):
if (
value.column_count != self.column_count
or value.row_count != self.row_count
):
raise ValueError(
f"expected {self.column_count!r}x{self.row_count!r},"
f" got {value.column_count!r}x{value.row_count!r}"
)
self._tictactoe = value
self.update_display()
def update_display(self):
for grid_row, button_row in zip(self.tictactoe.grid, self.buttons):
for cell_value, button in zip(grid_row, button_row):
button.config(
**(
{
"_": {
"highlightbackground": "WHITE",
"state": tk.NORMAL,
},
"1": {
"highlightbackground": "RED",
"state": tk.DISABLED,
},
"2": {
"highlightbackground": "GREEN",
"state": tk.DISABLED,
},
}[cell_value]
)
)
def place(self, row_index, column_index):
self.tictactoe.place(row_index, column_index)
self.update_display()
self.turn_callback()
class TicTacToeWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.tictactoe = TicTacToe()
self.player_label = tk.Label(self)
self.player_label.pack()
self.turn_label = tk.Label(self)
self.turn_label.pack()
self.board_ui = BoardUI(self, self.tictactoe, self.on_turn)
self.board_ui.pack()
self.start_new_game()
def update_display(self):
self.player_label["text"] = f"Player {self.tictactoe.current_player}"
self.turn_label["text"] = f"Turn {self.tictactoe.turn}"
def start_new_game(self):
self.tictactoe = TicTacToe()
self.board_ui.tictactoe = self.tictactoe
self.update_display()
def on_turn(self):
winner = self.tictactoe.check()
if winner is not None or self.tictactoe.is_full:
showinfo(
"Result",
"There is no winner"
if winner is None
else f"The winner is player {winner}",
)
self.start_new_game()
else:
self.update_display()
def main():
window = TicTacToeWindow()
window.mainloop()
if __name__ == "__main__":
main()