Zu dem Tic-Tac-Toe-Quelltext: Eingerückt wird mit vier Leerzeichen.
Es fehlt Leerraum um das lesbarer zu gestalten. Leerzeilen zwischen Funktionsdefinitionen und Leerzeichen um Operatoren, nach Kommas, und um Gleichheitszeichen bei Zuweisungen ausserhalb von Argumentlisten.
Die Zeilen in der `check()`-Funktion sind zu lang. Das ist nur ganz schwer lesbar.
Daten sollte man wie Code, möglichst nicht wiederholen. Da besteht die Gefahr, dass man Fehler macht und es macht Veränderungen aufwändiger und fehleranfälliger. Die Liste mit den beiden Symbolen (``["O", "X"]``) steht mehrfach im Quelltext.
Eine literale Liste mit drei leeren Listen ist nicht wirklich flexibel. Man würde da eher eine leere Liste erstellen und die dann in den Schleifen mit Listen füllen. Dann hängt das alles nur von der Anzahl der Schleifendurchläufe ab.
`button` ist ein passender Name für ein `Button`-Objekt, aber nicht für eine Funktion die solche Objekte erstellt. Funktionen werden üblicherweise nach der Tätigkeit benannt die sie durchführen.
Bei ``if not(i == a)`` sind die Klammern überflüssig und statt ``not`` und ``==`` würde man einfach ``!=`` schreiben.
`i` ist ein wirklich schlechter Name für eine Laufvariable die *keine* ganze Zahl ist.
In `reset()` sind die Schleifen über die Indexwerte ein „anti pattern“ in Python. Man kann direkt über die Elemente der Listen iterieren, ohne den Umweg über einen Indexwert.
`check()` macht deutlich mehr als nur zu prüfen. Das sollte nur prüfen und einen Rückgabewert liefern.
Die Zufallsauswahlt vom aktuellen Spieler und das aktualisieren des Labels wer gerade dran ist, steht beides zweimal im Quelltext, sollte da aber jeweils nur einmal stehen.
Statt `click()` die Indexwerte für Zeile und Spalte des Buttons zu übergeben, könnte man auch einfach das betreffende Button-Objekt selbst als Argument übergeben.
Zwischenstand:
Code: Alles auswählen
#!/usr/bin/env python3
#
# TODO Class to get rid of `StringVar` and passing so many arguments.
#
# TODO Don't mix UI and program logic.
#
import random
import tkinter as tk
from enum import auto, Enum
from functools import partial
from tkinter import messagebox
#
# TODO Use `Enum` to combine symbol and color into one object.
#
SYMBOLS = ["O", "X"]
SYMBOL_TO_COLOUR = {"O": "deep sky blue", "X": "lawn green"}
assert all(symbol in SYMBOL_TO_COLOUR for symbol in SYMBOLS)
assert len(SYMBOLS) == len(SYMBOL_TO_COLOUR)
class CheckResult(Enum):
NOT_WON = auto()
WON = auto()
DRAW = auto()
def create_button(master):
return tk.Button(
master,
width=3,
bd=10,
bg="papaya whip",
font=("arial", 60, "bold"),
padx=1,
relief="sunken",
)
def update_label(label, current_player_symbol_var):
label["text"] = f"{current_player_symbol_var.get()}'s Chance"
def reset_game(label, buttons, current_player_symbol_var):
for row in buttons:
for button in row:
button.config(text=" ", state=tk.NORMAL)
current_player_symbol_var.set(random.choice(SYMBOLS))
update_label(label, current_player_symbol_var)
def change_player(current_player_symbol_var):
assert len(SYMBOLS) == 2
for symbol in SYMBOLS:
if symbol != current_player_symbol_var.get():
current_player_symbol_var.set(symbol)
break
else:
raise ValueError(
f"unexpected symbol {current_player_symbol_var.get()!r}"
)
def check_for_win(buttons, current_player_symbol):
#
# TODO Rewrite this to be independent of actual size/shape of `buttons`.
#
for i in range(3):
if (
buttons[i][0]["text"]
== buttons[i][1]["text"]
== buttons[i][2]["text"]
== current_player_symbol
or buttons[0][i]["text"]
== buttons[1][i]["text"]
== buttons[2][i]["text"]
== current_player_symbol
):
return CheckResult.WON
if (
buttons[0][0]["text"]
== buttons[1][1]["text"]
== buttons[2][2]["text"]
== current_player_symbol
or buttons[0][2]["text"]
== buttons[1][1]["text"]
== buttons[2][0]["text"]
== current_player_symbol
):
return CheckResult.WON
if (
buttons[0][0]["state"]
== buttons[0][1]["state"]
== buttons[0][2]["state"]
== buttons[1][0]["state"]
== buttons[1][1]["state"]
== buttons[1][2]["state"]
== buttons[2][0]["state"]
== buttons[2][1]["state"]
== buttons[2][2]["state"]
== tk.DISABLED
):
return CheckResult.DRAW
return CheckResult.NOT_WON
def on_click(label, buttons, current_player_symbol_var, button):
symbol = current_player_symbol_var.get()
button.config(
text=symbol,
state=tk.DISABLED,
disabledforeground=SYMBOL_TO_COLOUR[symbol],
)
result = check_for_win(buttons, symbol)
if result in [CheckResult.WON, CheckResult.DRAW]:
if result is CheckResult.WON:
titel, text = "Congrats!!", f"{symbol!r} has won"
elif result is CheckResult.DRAW:
titel, text = "Tied!!", "The match ended in a draw"
else:
assert False, f"unexpected result {result!r}"
messagebox.showinfo(titel, text)
reset_game(label, buttons, current_player_symbol_var)
change_player(current_player_symbol_var)
update_label(label, current_player_symbol_var)
def main():
root = tk.Tk()
root.title("Tic-Tac-Toe")
current_player_symbol_var = tk.StringVar()
label = tk.Label(font=("arial", 20, "bold"))
label.grid(row=3, column=0, columnspan=3)
buttons = list()
for i in range(3):
row_buttons = list()
for j in range(3):
button = create_button(root)
button["command"] = partial(
on_click, label, buttons, current_player_symbol_var, button
)
button.grid(row=i, column=j)
row_buttons.append(button)
buttons.append(row_buttons)
reset_game(label, buttons, current_player_symbol_var)
root.mainloop()
if __name__ == "__main__":
main()
Das sollte man noch als Klasse umschreiben, damit man das `StringVar`-Objekt loswerden kann, und das so viele einzelne Argumente durch die Gegend gereicht werden müssen. Dann kann man auch Symbol und Farbe in jeweils einem Objekt zusammenfassen.
Die Prüffunktion enthält eine Menge magischer, fester Indexwerte. Das sollte man so umschreiben, dass es von den Dimensionen von `buttons` abhängt und nicht von hart kodierten Zahlen.
@__deets__: Der zweite ``import`` ist notwendig weil `messagebox` ein eigenes (Unter)Modul ist das durch den *-Import nicht unbedigt erfasst wird. Das hängt wohl von der Python-Version ab ob `tkinter.messagebox` von `tkinter` importiert wird, und damit durch das * mit kommt, oder nicht.