von Michael V. und Paul V.
Das Projekt besteht hauptsächlich aus den beiden Dateien main.py
und game.py
. Das Skript main.py
enthält das Hauptmenü und die Zusammenführung der Spiellogik, während game.py
die Spiellogik ansich enthält.
Zunächst werden alle benötigten Module importiert in Zeile 4 bis 6:
from tkinter import *
from tkinter import font
from game import Game
Dann wird das Hauptmenüfenster erstellt und auch ein entsprechender Titel für das Fenster gesetzt. In Zeile 15 werden die Schriftarten (fonts) definiert, um einen einheitlichen Look zu erhalten:
# Fonts definieren
TITLE = font.Font(family="Arial", size=18, weight="bold")
BUTTON = font.Font(family="Arial", size=12)
Im Anschluss wird das Herzstück des Hauptfensters abgefasst, nämlich der Titel und die beiden Buttons, die zu den beiden verfügbaren Spielmodi führen.
title_main_window = Label(main_window, text="4 gewinnnt!", font=TITLE)
option_game_multiplayer = Button(
main_window,
text="2 Spieler",
width=12,
height=2,
font=BUTTON,
command=lambda: Game(2),
)
option_game_singleplayer = Button(
main_window,
text="Einzelspieler",
width=12,
height=2,
font=BUTTON,
command=lambda: Game(1),
)
Die beiden Buttons sind auch mit einem Command belegt, welcher über eine anonyme (Lambda-) Funktion eine Instanz der Klasse Game
erstellt, wobei der Spielmodus als Parameter übergeben wird.
Zuletzt werden die drei Elemente mit Hilfe der pack
-Methode auf dem Hauptfenster angeordnet und das Fenster selbst mit mainloop
gestartet:
# Elemente platzieren
title_main_window.pack(pady=10)
option_game_multiplayer.pack()
option_game_singleplayer.pack()
# MAINLOOP
main_window.mainloop()
Bei der Ausführung dieses Codes wird ein Fenster mit einem Titel und zwei Buttons angezeigt. Hier auch interessant Parameter bei der Platzierung des Fenstertitels pady
, welcher lediglich den Abstand zwischen dem Text bzw. Inhalt des Labels und dem Rand des Fensters erhöht. Die beiden Buttons werden direkt untereinander angeordnet.
Die Spiellogik befindet sich hier in dieser Datei, genauergesagt in der Kl
Klasse Game
. Jedes Mal, wenn im Hauptmenü einer der beiden Buttons gedrückt
wird, die ein Spiel starten sollen, wird eine Instanz der Klasse erstellt.
Zunächst werden die notwendigen Imports durchgeführt, um die erforderlichen Module bereitzustellen (Zeilen 1-4):
from tkinter import *
from tkinter.messagebox import showinfo
from tkinter.ttk import Combobox
import random
tkinter
: Modul für die Erstellung von GUIstkinter.messagebox
: Untermnodul vontkinter
für die Erstellung von Pop-Up-Fensterntkinter.ttk
: Untermmodul vontkinter
für die Erstellung von Drop-Down-Menüselbsrandom
: Modul, womit man zufällige Werte generieren kann
Ab Zeile 8 wird die Klasse Game
definiert:
class Game:
def __init__(self, gt):
self.game_window = Tk()
self.game_window.geometry("850x800")
self.current_player = 1 # Spieler 1 beginnt
self.game_over = False
self.gt = gt
Die __init__
-Methode, auch als Konstruktur bekannt, ist eine Methode, die ein Mal bei der Instanzierung
der Klasse aufgerufen wird. Sie kann auch Parameter nehmen, wie hier gt
. Diese Parameter
werden einfach bei der Instanzierung der Klasse übergeben in den Klammern (Game(1)
).
Variable | Datentyp | Verwendung |
---|---|---|
self.game_window |
Tk (Objekt) | Root-Objekt für das Spielfenster |
self.current_player |
Integer | Angabe aktueller Spieler |
self.game_over |
Boolean | Angabe, ob das Spiel beendet ist |
self.gt |
Integer | Parameter, der übergeben bei der Instanzierung |
Im Anschluss haben wir das zusätzliche Feature eingebaut, dass man die Farben für die Spieler festlegen kann.
Dabei kommt die Combox
-Klasse zum Einsatz. Hiermit wird gleich ein Drop-Down-Menü erstellt,
in dem man die Farben auswählen kann.
# Farbwahl
self.color_window = Tk()
self.color_window.geometry("500x200")
self.color_window_label = Label(
self.color_window, text="Bitte wähle die Farben!"
)
Hier wird das Fenster für die Farbwahl erstellt. Im Anschluss werden die Fensterdimensionen festegelegt, sowie ein Label erstellt, was als Überschrift für das Fenster fungiert.
self.colors = ["red", "yellow", "blue", "green"]
Das ist die Liste, die die verfügbaren Farben enthält. Diese Liste kann beliebig erweitert werden.
self.color_window_label_1 = Label(self.color_window, text="Spieler 1")
self.color_window_label_2 = Label(self.color_window, text="Spieler 2")
self.drop1 = Combobox(self.color_window, values=self.colors)
self.drop2 = Combobox(self.color_window, values=self.colors)
Hier werden die Labels für die Drop-Down-Menüs erstellt, sowie die Drop-Down-Menüs selbst für die Farbauswahl.
Als Paramater für das Drop-Down-Menü übergebe ich in values
die Liste colors
der verfügbaren Farben.
self.OK_button = Button(
self.color_window,
text="OK",
command=lambda: self.check_colors(
color1=self.drop1.get(), color2=self.drop2.get()
),
)
Hier wird der OK-Button erstellt, der das Fenster schließt, wenn Farben ausgewählt wurden. Wenn keine oder gleiche Farben
ausgewählt werden, dann schließt er das Fenster nicht. Wie die evaluierende Methode check_colors
funktioniert, wird in Kürze erläutert.
An dieser Stelle könnte man implementieren, dass man das Fenster nicht mit den normalen Schaltflächen am Bildschirmrand schließen kann.
self.color_window_label.grid(column=1, row=0)
self.color_window_label_1.grid(column=0, row=1)
self.color_window_label_2.grid(column=2, row=1)
self.drop1.grid(column=0, row=2)
self.drop2.grid(column=2, row=2)
self.OK_button.grid(column=1, row=3)
Zuletzt werden nochmal die ganzen Elemente mithilfe der grid
-Methode geordnet im Fenster platziert.
In Relation zu den Fensterdimensionen werden die Elemente wie in einer Tabelle angeordnet mit Zeilen und Spalten.
Die __init__
-Methode endet mit der Evaluierung der gt
-Variable und dem Aufruf der Methode drawBoard
:
if self.gt == 1:
self.game_window.title("Gegen Computer - Spiel")
else:
self.game_window.title("Mehrspieler - Spiel")
self.drawBoard()
Ersteres bestimmt den Titel des Spielfensters. Die drawBoard
-Methode` wird verwendet, um das Spielfeld zu
zeichnen. Mehr dazu in Kürze.
def check_colors(self, color1, color2):
if color1 == color2:
return
else:
self.color_player1 = color1
self.color_player2 = color2
self.color_window.destroy()
Die Methode check_colors
wird aufgerufen, wenn der OK-Button gedrückt wird. Sie überprüft, ob die beiden ausgewählten
Farben gleich sind. Wenn ja, dann wird nichts zurückgegeben (Es passiert nichts :o). Ansonsten werden die beiden
lokalen Variablen in Instanzvariablen gespeichert. Das Fenster wird geschlossen durch self.color_window.destroy()
.
Mit drawBoard
-Methode wird das Feld gezeichnet. Zunächst wird ein zweidimensionales Array
generiert:
self.board = [[0 for _ in range(6)] for _ in range(7)]
Also es wird für jede Zeile ein Array mit 6 Elementen erstellt. Das Ganze sieben Mal, weil es gibt 7 Spalten.
Für jede Spalte wird ein Button erstellt, den man klicken kann, um seinen Spielstein zu platzieren:
for col in range(7):
col_button = Button(
self.game_window,
text=col + 1,
width=8,
height=5,
command=lambda c=col: self.drop_piece(c),
)
col_button.grid(row=0, column=col)
Jeder dieser Buttons wird aucht mit der drop_piece
-Methode belegt, damit dann auf der Canvas (Spielfeld)
ein Spielstein in entsprechender Farbe plaziert wird.
Ein weiteres zusätzliches Feature, was wir eingebaut haben, ist die Anzeige des aktuellen Spielers:
self.current_player_label = Label(
self.game_window, text=f"Spieler {self.current_player} ist am Zug"
)
self.current_player_label.grid(row=0, column=7, columnspan=7)
Für die grafische Umsetzung des Feldes wird eine Canvas (Built-in-Feature von Tkinter) erstellt:
self.canvas = Canvas(
self.game_window,
width=700,
height=800,
)
self.canvas.grid(row=1, column=0, columnspan=7)
self.cell_width = 100
self.cell_height = 100
for col in range(7):
for row in range(6):
x1 = col * self.cell_width
y1 = row * self.cell_height + 50
x2 = x1 + self.cell_width
y2 = y1 + self.cell_height
self.canvas.create_rectangle(
x1, y1, x2, y2, outline="black", fill="white"
)
In der letzten for-Loop werden die einzelnen, später befüllten, Kästchen generiert.
Die entscheidende Methode, die die Spiellogik steuert im Endeffekt, ist die drop_piece
-Methode:
def drop_piece(self, col):
# wenn zeile voll
if self.board[col][0] != 0:
showinfo("Fehler", "Diese Spalte ist bereits voll!")
return
Dieser erste Ausschnitt der Methode prüft, ob die als Parameter mitgegebene Spalte bereits voll ist. Dort kann logischerweise kein Stein mehr platziert werden.
# Finde die nächste freie Zeile in der gewählten Spalte
for row in range(5, -1, -1):
if self.board[col][row] == 0:
self.board[col][row] = self.current_player
self.draw_piece(col, row)
if self.check_winner(col, row):
print(f"Spieler {self.current_player} hat gewonnen!")
showinfo(
"Game Over",
f"Das Spiel ist vorbei! Spieler {self.current_player} hat gewonnen.",
)
self.game_window.destroy()
return
self.current_player = (
2 if self.current_player == 1 else 1
) # Wechseln des Spielers
self.current_player_label.config(
text=f"Spieler {self.current_player} ist am Zug"
)
break
Die "große" for-Schleife beginnt bei dem Wert 5 (Zeile 6) und geht immer einen Schritt nach unten (-1),
bis sie eine freie Zeile in der gewählten Spalte gefunden hat. Im Anschluss wird die Zahl des aktuellen Spielers
in das zweidimensionale Array self.board
eingetragen und die Methode draw_piece
wird aufgerufen, welche dann den Spielstein
malt.
Die draw_piece
-Methode ist auch kein Hexenwerk; es zeichnet einfach einen Kreis in das jeweilige Kästchen mit
den bereits festegelegten Farben.
def draw_piece(self, col, row):
x1 = col * self.cell_width
y1 = row * self.cell_height + 50
x2 = x1 + self.cell_width
y2 = y1 + self.cell_height
color = self.color_player1 if self.current_player == 1 else self.color_player2
self.canvas.create_oval(x1, y1, x2, y2, fill=color)
Es gibt ja keine Spielschleife / Hauptschleife im klassischen Sinne. Wir haben es so gelöst, dass jedes Mal,
wenn ein Stein gesetzt wurde, auch direkt geprüft wird, ob ein Spieler gewonnen hat. Dazu haben wir die Methode check_winner
erstellt:
def check_winner(self, col, row):
# Überprüfen auf Gewinnbedingungen (horizontal, vertikal, diagonal)
return (
self.check_direction(col, row, 1, 0) # horizontal
or self.check_direction(col, row, 0, 1) # vertikal
or self.check_direction(col, row, 1, 1) # diagonal
or self.check_direction(col, row, 1, -1) # diagonal
)
Die hier mehrfach aufgerufene Methode check_direction
prüft, ob es in einer bestimmten Richtung mehrere Spielsteine des
gleichen Spielers gibt:
def check_direction(self, col, row, delta_col, delta_row):
count = 0 # Zähle den aktuellen Stein
# Überprüfen in positiver Richtung
for i in range(-3, 4, 1):
new_col = col + i * delta_col
new_row = row + i * delta_row
if 0 <= new_col < 7 and 0 <= new_row < 6:
if self.board[new_col][new_row] == self.current_player:
count += 1
if count >= 4:
return True
else:
count = 0
return False
Diese Methode ist sehr interessant und zwar dahingehend, dass sie neben den Paramtern col
und row
auch noch zwei weitere
Parameter nimmt: delta_col
und delta_row
. Diese beiden Parameter bestimmen die Richtung, in welche Richtung
die Methode prüfen soll. Die Bezeichnung delta wurde hier verwendet aufgrund des Kontextes in der Mathematik und Physik,
wo der griechische Buchstabe Delta (Δ) oft verwendet wird, um eine Änderung oder Differenz darzustellen.
Zurück zur Methode drop_piece
:
# Finde die nächste freie Zeile in der gewählten Spalte
for row in range(5, -1, -1):
if self.board[col][row] == 0:
self.board[col][row] = self.current_player
self.draw_piece(col, row)
if self.check_winner(col, row):
print(f"Spieler {self.current_player} hat gewonnen!")
showinfo(
"Game Over",
f"Das Spiel ist vorbei! Spieler {self.current_player} hat gewonnen.",
)
self.game_window.destroy()
return
self.current_player = (
2 if self.current_player == 1 else 1
) # Wechseln des Spielers
self.current_player_label.config(
text=f"Spieler {self.current_player} ist am Zug"
)
break
Falls die Methode check_winner
einen wahren Wert zurückgibt, dann wird ein Popup-Fenster erstellt,
wo der Sieger bekannt gegeben wird. Das Spiel wird dann beendet und das Fenster wird geschlossen. Insofern
noch keiner der beiden Spieler gewonnen hat, dann wird der aktuelle Spieler getauscht und diese Änderung
im Label auf dem Bildschirm angezeigt.
Wie man aber vielleicht schon ahnen konnte bei der Variable self.gt
, müsste es ja mehrere Spielmodi geben.
Und ja, es gibt die Möglichkeit, nicht gegen einen zweiten Spieler zu spielen, sondern auch gegen den Computer!
if self.gt == 1 and self.current_player == 2:
column = random.choice([col for col in range(7) if self.board[col][0] == 0])
self.drop_piece(column)
Da der Schwerpunkt nicht auf der Entwicklung einer KI für das Spiel lag, haben wir einfach festgelegt, dass der Computer immer auf ein zufälliges Feld seine Spielsteine platziert.
Zuletzt wird jedoch noch auf ein Unentschieden geprüft. Wenn alle Felder auf dem Spielbrett besetzt sind bzw. es wird nur die oberste Zeile geprüft, ob da alle Felder besetzt sind, dann ist das Spiel unentschieden.
if all(self.board[col][0] != 0 for col in range(7)):
print("Unentschieden!")
showinfo("Game Over", "Das Spiel ist unentschieden.")
self.game_window.destroy()
Am Rande kann man noch die __repr__
-Methode erwähnen, die lediglich dazu dient, einen String zurückzugeben,
der dann in der Kommandozeile ausgegeben wird, wenn man die Variable der Instanz printen möchte. Sonst
würde da nur eine Objektbezeichnung kommen mit der Speicherstelle im Arbeitsspeicher.
def __repr__(self) -> str:
return f"{self.board}"
Hier wird einfach das mehrdimensionale Array ausgegeben, was die Spielbrett darstellt.
Wenn man das Spiel ganz normal spielen möchte, dann am besten sicherstellen, dass man Python 3 installiert hat.
Des Weiteren muss man die Datei main.py
ausführen.
Sobald man dann einen Spielmodi ausgewählt hat, kann es passieren, dass das Farbauswahlfenster dem Spielfenster ist. Nicht minimieren, sondern einfach, das Spielfenster beseite ziehen und die Eingaben tätigen.
Im Spiel einfach immer den Button über der Spalte anklicken, wo man seinen Spielstein platzieren möchte.
Das Programm hat keine bekannten Bugs (10.12.2024).
Michael - Grundspiel Paul - alle zusätzlichen Features (Farbauswahl, Anzeige akt. Spieler, Spiel gegen Computer)
Wir wünschen viel Spaß beim Spielen!