K103: Tic Tac Toe (Fallbeispiel)

In diesem Blogbeitrag lernst du, das Spiel Tic-Tac-Toe in Python mit Turtle Graphics zu programmieren.

Table of Contents

Tic Tac Toe

Tic-Tac-Toe (auch Drei gewinnt, Kreis und Kreuz, Dodelschach) ist ein einfaches Strategiespiel, dessen Geschichte sich bis ins 12. Jahrhundert v. Chr. zurückverfolgen lässt.

Gegeben ist ein quadratischen, 3×3 Felder grosses Spielfeld. Beide Spieler setzend abwechselnd ihre Steine bzw. Zeichen (ein Spieler setzt Kreuze, der andere setzt Kreise) in ein freies Feld. Der Spieler, welcher als Erster drei Zeichen in eine Zeile, Spalte oder Diagonale setzen kann, gewinnt. Nachfolgend ist ein möglicher Spielverlauf (von links nach rechts) aufgezeichnet. Der Spieler mit dem Kreuz beginnt und gewinnt nach insgesamt 5 Zügen (3 Züge Spieler X und 2 Züge Spieler O), da in der ersten Zeile 3 X-Steine nebeneinander liegen.

Spielen beide Spieler optimal, so kann keiner gewinnen und es kommt zu einem Unentschieden. 

Grundversion

Allgemeine Vorbereitungen

Als erstes müssen wir unsere turtle Bibliothek importieren und den Bildschirm vorbereiten. Wir wollen das nachfolgend dargestellte Fenster erzeugen. Es hat den Fenstertitel «Tic Tac Toe» und eine Grösse von 700 x 700 Punkten.

Um das Fenster zu erstellen benötigen wir folgenden Code:
# Bibliothek importieren
from turtle import *

# Bildschirm vorbereiten
setup(width = 700, height = 700)
title("Tic Tac Toe")

# hier kommt dann unser Programm..
# ....

# Ende
exitonclick()

Am Anfang importieren wir allen Befehlen aus der turtle Bibliothek (Zeile 2: from turtle import *).  Anschliessend erstellen wir unser Fenster (Screen). Die Anweisung setup() stellt die Grösse unseres Bildschirmfensters ein. Wir wählen eine Breite (width) von 700 Punkten und ebenso eine Höhe (height) von 700 Punkten. Mit title() können wir den Fenstertitel festlegen. In unserem Beispiel soll «Tic Tac Toe» oben auf dem Fensterbalken stehen (Zeile 6).

Am Ende des Programms schreiben wir in gewohnter Weise exitonclick(), so dass unser Fenster erst wieder verschwindet, wenn der Benutzer mit der linken Maustaste auf das Fenster geklickt hat.

Spielfeld (Gitter) malen

Als nächstes wollen unser Spielfeld (Gitter) zeichnen. Es besteht aus 9 Feldern. Bei Tic-Tac-Toe zeichnet man hierzu meist nur 2 horizontale und 2 vertikale Striche, lässt also die Umrandung weg (siehe nachfolgendes Bild, linker Teil). Jedes Feld soll eine Grösse von 100 x 100 Punkten haben. Das ganze Gitternetz hat damit eine Grösse von 300 x 300 Punkten. Er soll um unsere Startposition in der Mitte des Bildschirms (Ursprung) herum gezeichnet werden. Die nachfolgende Grafik skizziert unser Spielfeld. Links, so wie es auf dem Bildschirm aussehen soll, rechts mit entsprechenden Massangaben.

Nachfolgend findest du den passenden Code. In den Zeilen 8 und 9 verstecken wir als erstes unseren Cursor (unsere Schildkröte) mit der hideturtle() Anweisung und erhöhen das Zeichentempo (Bewegungstempo der Schildkröte) mit speed(0). Anschliessend beginnen wir mit dem Zeichnen der Linien. Beachte: Unsere Schildkröte blickt anfangs nach rechts. Aus diesem Grund beginnen wir mit den horizontalen Linien und drehen die Schildkröte um 90 Grad nach links, bevor wir die vertikalen Linien zeichnen.

from turtle import *

# Bildschirm
setup(width = 700, height = 700)
title("Tic Tac Toe")

# Turtle
hideturtle()
speed(0)

# Gitter - horizontale Linien
penup()
goto(-150,-50)    # untere Linie
pendown()
forward(300)
penup()
goto(-150,+50)    # obere Linie
pendown()
forward(300)

# Gitter -- vertikale Linien
left(90)
penup()
goto(-50,-150)    # untere Linie
pendown()
forward(300)
penup()
goto(+50,-150)    # obere Linie
pendown()
forward(300)

# Ende
exitonclick()

Das sind ganz schön viele Zeilen. In der Praxis würde man die einzelnen Linien eher in zwei for Schleifen verpacken. Die erste soll alle horizontalen Linien zeichnen, die zweite anschliessend alle vertikalen Linien. Im nachfolgenden Beispielcode haben wir zudem mit der Anweisung pensize(4) die Liniendicke noch auf 4 Punkte erhöht (siehe Zeile 10).

from turtle import *

# Bildschirm
setup(width = 700, height = 700)
title("Tic Tac Toe")

# Turtle
hideturtle()
speed(0)
pensize(4) # Gitter - horizontale Linien # - horizontale Linien for i in range(2): penup() goto(-150,-50+i*100) pendown() forward(300) # Gitter - vertikale Linien left(90) for i in range(2): penup() goto(-50+i*100,-150) pendown() forward(300) # Ende exitonclick()

Neben dem Zeichnen des Gitters kann es zudem vorteilhaft sein, die einzelnen Felder zu beschriften. Das soll dann die Orientierung für den Benutzer etwas erleichtern. Hierzu müssen wir unseren Code wie folgt ergänzen:

# Beschriften
penup()
for j in range(3):
    for i in range(3):
        goto(-130+i*100, 120-j*100)
        write(j*3+i+1, align="center", font=("Arial", 14, "normal"))

Im Code haben wir 2 for Schleifen. Die erste steht für die Zeile, die zweite (verschachtelte) für die Spalte. Beide Schleifen werden 3x durchlaufen, wobei die Variablen die Werte 0, 1 und 2 annehmen. 

Spieler setzen lassen

Nun geht’s los. Wir wollen unsere Spieler die Steine setzen lassen. Der Einfachheit halber gehen wir davon aus, dass immer der X-Spieler beginnt.

Lassen wir vorerst mal unsere Spieler je 4 Züge machen. Mit je 4 Zügen sind 8 der 9 Felder belegt und der Spielausgang eigentlich schon klar (es kann ja nur noch 1 Person 1 Stein setzen). Hierzu erstellen wir in bekannter Weise eine while Schleife mit einer Zählvariable züge. Die zu wiederholenden Anweisungen sind eingeschoben. Am Anfang der Schleife steht der Schleifenkopf (Zeile 2) mit der Bedingung. Vor dem Schleifenkopf wird die Variable auf 0 gesetzt. Das ist zwingend notwendig. Wir können nicht im nachfolgenden Schleifenkopf einen Vergleich anstellen (züge < 4), ohne vorab Python zu sagen, was genau züge ist. Innerhalb der Schleife (hier in Zeile 3) erhöhen wir dann die Zählvariable bei jeder Wiederholung um 1. Dadurch wird unsere Schlaufe genau 4x durchlaufen (züge = 0, züge = 1, züge = 2, züge = 3).

züge = 0
while züge < 4:
    züge = züge + 1
    # Spieler X
    # ....
    
    # Spieler O    
    # ....

Was sollen die Spieler 4 mal machen? Wir lassen sie jeweils eine Zahl zwischen 1 und 9 für das gewünschte Feld eingeben. Hierzu können wir die Anweisung numinput() verwenden. Damit man das Spiel nicht bis zum Ende spielen muss, ergänzen wir noch die Möglichkeit, mit der Zahl 0 das Spiel (die Wiederholungen) abzubrechen. Mit der break Anweisung kann man eine Schlaufe jederzeit verlassen.

züge = 0
while züge < 4:
    züge = züge + 1
    # Spieler X
    x = int(numinput("Spieler X", "Bitte Feld eingeben: "))
    if x == 0:
        break

    # Spieler O
    x = int(numinput("Spieler O", "Bitte Feld eingeben: "))
    if x == 0:
        break

Jetzt müssen wir nur noch die Zeichen in unser Spielfeld setzen, so dass der Spieler auch sieht, wo er seinen Stein gesetzt hat. Eine Möglichkeit besteht darin, einfach mit der write() Anweisung ein grosses «X» oder ein grosses «O» zu schreiben (siehe nachfolgend Zeilen 12 und 21):

züge = 0
while züge < 4:
    züge = züge + 1

    # Spieler X
    x = int(numinput("Spieler X", "Bitte Feld eingeben: "))
    if x == 0:
        break
    spalte = (x-1) % 3
    zeile = (x-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write("X",font=('Arial', 70, 'normal'))

    # Spieler O
    x = int(numinput("Spieler O", "Bitte Feld eingeben: "))
    if x == 0:
        break
    spalte = (x-1) % 3
    zeile = (x-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write("O",font=('Arial', 70, 'normal'))

Der etwas schwierigere Teil des Codes liegt in der Berechnung der richtigen Stelle. Denn wissen wir erst einmal, in welcher Zeile und Spalte sich das Feld befindet, dann können wir mittels goto(x,y) an die richtige Stelle fahren (siehe Zeilen 11 und 20 direkt vor den write() Anweisungen). Zur Berechnung der richtigen Zeile und Spalte kann man den Modulo Operator % und den Gannzahl Operator // nutzen.

Der Operator für die Ganzzahl ermittelt die Zeile (hier beginnend bei Zeile 0), der Modulo die Spalte (hier beginnend bei Spalte 0). Idealerweise machst du zu beginn ein paar Rechenbeispiele von Hand. Nachfolgend findest du zwei Tabellen mit fortlaufenden Zahlen und 3 bzw. 4 Spalten. Dividiert Nummer durch die Anzahl Spalten der Tabelle, so repräsentiert die Ganzzahl die Zeile und der Rest (Modulo) die Spalte. 3 : 3 = 1 Rest 0, 5 : 3 = 1 Rest 2 etc.

Vollständiger Code

Nachfolgend findest du den vollständigen Code der Grundversion. 

from turtle import *

# Bildschirm
setup(width = 700, height = 700)
title("Tic Tac Toe")

# Turtle
hideturtle()
speed(0)

# Gitter zeichnen
# - horizontale Linien
pensize(4)
for i in range(2):
    penup()
    goto(-150,-50+i*100)
    pendown()
    forward(300)

# - vertikale Linien
left(90)
for i in range(2):
    penup()
    goto(-50+i*100,-150)
    pendown()
    forward(300)

# - Beschriften der Felder
penup()
for j in range(3):
    for i in range(3):
        goto(-130+i*100, 120-j*100)
        write(j*3+i+1, align="center", font=("Arial", 14, "normal"))

# Spiel      
züge = 0
while züge < 4:
    züge = züge + 1

    # Spieler X
    x = int(numinput("Spieler X", "Bitte Feld eingeben: "))
    if x == 0:
        break
    spalte = (x-1) % 3
    zeile = (x-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write("X",font=('Arial', 70, 'normal'))

    # Spieler O
    x = int(numinput("Spieler O", "Bitte Feld eingeben: "))
    if x == 0:
        break
    spalte = (x-1) % 3
    zeile = (x-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write("O",font=('Arial', 70, 'normal'))

# Ende
exitonclick() 

Erweiterungen

Unsere Spiel lässt sich erweitern und verbessern. Nachfolgend noch zwei Ergänzungen.

Spieler und Züge

Eine erste Erweiterung wollen wir unser Programm dahingehend ändern, dass jeder Zug einzeln zählt und nach jedem Zug dann der jeweils andere Spieler wieder an die Reihe kommt.

Bisherige Version:

züge = 0
while züge < 4:
    züge = züge + 1
    # Spieler X
    # ....
    
    # Spieler O    
    # ....

Neue Version:

züge = 0
while züge < 9:
    züge = züge + 1
    # Spieler abwechselnd
    # ....
    

Wir definieren dafür die Variable zug_x. Sie soll angeben, ob der Spieler X an der Reihe ist. Studiere das nachfolgende Programm.

# Spiel: los geht's
züge = 0
zug_x = "ja"

while züge < 9:
    züge = züge + 1
    if zug_x == "ja":
        beschreibung = "Spieler X"
        zeichen = "X"
    else:
        beschreibung = "Spieler O"
        zeichen = "O"

    # "Stein" setzen
    feld = int(numinput(beschreibung, "Bitte Feld eingeben: "))

    ....
    
    write(zeichen,font=('Arial', 70, 'normal'))

    # Spieler wechseln
    if zug_x == "ja":
        zug_x = "nein"
    else:
        zug_x = "ja"

In unserem Beispiel startet der Spieler X, weshalb wir auch gleich vor dem ersten Zug bzw. vor dem Schlaufenkopf zug_x = «Ja» setzen (in Zeile 3). In Zeile 7 untersuchen wir dann, ob der Spieler X an der Reihe ist. Falls ja, wird die Variable beschreibung auf «Spieler X» und die Variable zeichen auf «X» gesetzt. Die erste Variable nutzen wir, um das Eingabefenster richtig zu bezeichnen (siehe bei numinput() in Zeile 15), die zweite Variable, um das richtige Zeichen zu schreiben (siehe bei write() in Zeile 19). Ganz am Ende, in den Zeilen 21-25, wechseln wir dann den Spieler.

Etwas schöner ist es übrigens, wenn wir anstelle von «ja» und «nein» die Wörter True und False verwenden. zug_x ist dann anstelle einer Zeichenkette (Objekt vom Datentyp str) ein sog. Wahrheitswert (Objekt vom Typ bool). Es ist vor allem deshalb schöner, weil wir einen Wahrheitswert mit dem not Operator umkehren können. Damit lassen sich die oben erwähnten Zeilen 21-25 auf einer einzelnen Zeile zusammenfassen.

Version mit «ja» und «nein» (Datentyp str):

...
zug_x = "ja"

while züge < 9:
    ...
    if zug_x == "ja":
        ...

    # Spieler wechseln
    if zug_x == "ja":
        zug_x = "nein"
    else:
        zug_x = "ja"

Kürzere Version:

...
zug_x = True

while züge < 9:
    ...
    if zug_x == True:
        ...

    # Spieler wechseln
    zug_x = not(zug_x)

Nachfolgend nochmals der ganze (mittlere Teil) Code:

# Spiel: los geht's
züge = 0
zug_x = True

while züge < 9:
    züge = züge + 1
    if zug_x == True:
        beschreibung = "Spieler X"
        zeichen = "X"
    else:
        beschreibung = "Spieler O"
        zeichen = "O"

    # "Stein" setzen
    feld = int(numinput(beschreibung, "Bitte Feld eingeben: "))
    if feld == 0:
        break
    spalte = (feld-1) % 3
    zeile = (feld-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write(zeichen,font=('Arial', 70, 'normal'))

    # Spieler wechseln
    zug_x = not(zug_x)

Gewinner ermitteln

Als zweites möchten wir, dass der Computer für uns automatisch erkennt, sobald ein Spieler gewonnen hat. Es gibt viele Ansätze, wie man so etwas programmieren kann. Wir machen es uns relativ einfach und

Liste erstellen

Um überprüfen zu können, ob ein Spieler gewonnen hat, müssen wir wissen, auf welchem Feld welcher Stein liegt. Grundsätzlich haben wir zwar auf dem Bildschirm grosse X und O geschrieben, aber eine Auswertung dieser graphischen Symbole ist schwierig. Wir benötigen deshalb ein Hilfsmittel. In unserem Fall ist es eine einfache Liste, in welcher alle Züge eingetragen werden.

Am Anfang des Spiels (vor dem Schleifenkopf) definieren wir eine leere List. Damit es etwas schöner aussieht, erstellen wir in unserem Beispiel eine Liste mit lauter Minuszeichen. Für jedes Feld auf dem Spielfeld hat die Liste 1 Element. Wir können die Liste entweder aufzählen (es sind ja nur 9 Elemente), oder abgekürzt ein Element auch mit 9 multiplizieren.

g = ["-", "-", "-", "-", "-", "-", "-", "-","-",]

g = ["-"] * 9

Die Idee dahinter ist dann, dass bei jedem Zug das jeweilige Symbol in die Liste geschrieben wird. Das erste Element in der Liste entspricht dem ersten Feld auf dem Spielfeld, das zweite Element dem zweiten usw. Du erinnerst dich an unser Beispiel ganz oben: 

Für unser einleitendes Beispiel ergibt sich die folgende Entwicklung:

  1. [«X», «-«, «-«, «-«, «-«, «-«, «-«, «-«, «-«]
  2. [«X», «-«, «-«, «-«, «O», «-«, «-«, «-«, «-«]
  3. [«X», «X», «-«, «-«, «O», «-«, «-«, «-«, «-«]
  4. [«X», «X», «-«, «-«, «O», «-«, «O», «-«, «-«]
  5. [«X», «X», «X», «-«, «O», «-«, «O», «-«, «-«]

Über die sogenannte Indexnotation können wir leicht Elemente in die Liste schreiben (bzw. bestehende «-» überschreiben) und später auch wieder auslesen. Zu beachten ist dabei nur, dass der Index bei 0 beginnt. Das erste Element hat folglich die Nummer 0, etc.

# Spiel: los geht's
züge = 0
zug_x = True
g = ["-"]*9

while züge < 9:
    züge = züge + 1
    ....
# "Stein" setzen feld = int(numinput(beschreibung, "Bitte Feld eingeben: ")) g[feld-1] = zeichen ...

Kontrolle, ob gewonnen

Nun müssen wir lediglich noch kontrollieren, ob jemand gewonnen hat. Wie erläutert schauen wir nach jedem Zug, ob dieser Zug zum Sieg geführt hat. Vorab definieren wir noch eine Variable gew (für gewonnen). Wir lassen unsere while Schleife dann so lange wiederholen, bis 9 Züge durch sind oder ein Spieler gewonnen hat. 

# Spiel: los geht's
...
gew = False

while züge < 9 and gew != True:
    ...  

    # Spieler wechseln
    zug_x = not(zug_x)

Gewonnen hat jemand, wenn:

  • in der aktuellen Zeile 3 seiner Steine sind.
  • in der aktuellen Spalte 3 seiner Steine sind.
  • in einer der beiden Diagonalen 3 seiner Steine sind.

Es gilt somit folgendes zu überprüfen:

while züge < 9 and gew != True:
   ...

    # "Stein" setzen
    feld = int(numinput(beschreibung, "Bitte Feld eingeben: "))

    spalte = (feld-1) % 3
    zeile = (feld-1) // 3
    ...
    if g[zeile*3] == zeichen and g[zeile*3+1] == zeichen and g[zeile*3+2] == zeichen:
        gew = True
    if g[spalte] == zeichen and g[spalte+3] == zeichen and g[spalte+6] == zeichen:
        gew = True
    if g[0] == zeichen and g[4] == zeichen and g[8] == zeichen:
        gew = True
    if g[2] == zeichen and g[4] == zeichen and g[6] == zeichen:
        gew = True    

Zusammenfassend ergibt sich der nachfolgende Code. Am Ende wir noch ausgegeben, wer gewonnen hat. Viel Spass!

from turtle import *

# Bildschirm
setup(width = 700, height = 700)
title("Tic Tac Toe")

# Turtle
hideturtle()
speed(0)

# Gitter  
# - horizontale Linien
pensize(4)
for i in range(2):
    penup()
    goto(-150,-50+i*100)
    pendown()
    forward(300)

# - vertikale Linien
left(90)
for i in range(2):
    penup()
    goto(-50+i*100,-150)
    pendown()
    forward(300)

# - Felder beschriften
penup()
for j in range(3):
    for i in range(3):
        goto(-130+i*100, 120-j*100)
        write(j*3+i+1, align="center", font=("Arial", 14, "normal"))
    
# Spiel: los geht's
züge = 0
gew = False
zug_x = True
g = ["-"]*9

while züge < 9 and gew != True:
    züge = züge + 1
    if zug_x == True:
        beschreibung = "Spieler X"
        zeichen = "X"
    else:
        beschreibung = "Spieler O"
        zeichen = "O"

    # "Stein" setzen
    feld = int(numinput(beschreibung, "Bitte Feld eingeben: "))
    if feld == 0:
        break
    g[feld-1] = zeichen

    spalte = (feld-1) % 3
    zeile = (feld-1) // 3
    goto(-130+spalte*100, 50-zeile*100)
    write(zeichen,font=('Arial', 70, 'normal'))

    if g[zeile*3] == zeichen and g[zeile*3+1] == zeichen and g[zeile*3+2] == zeichen:
        gew = True
    if g[spalte] == zeichen and g[spalte+3] == zeichen and g[spalte+6] == zeichen:
        gew = True
    if g[0] == zeichen and g[4] == zeichen and g[8] == zeichen:
        gew = True
    if g[2] == zeichen and g[4] == zeichen and g[6] == zeichen:
        gew = True    

    # Spieler wechseln
    zug_x = not(zug_x)


# und der Gewinner ist 
goto(-150,-200)
if gew == True:
    write("Gewonnen hat " + beschreibung, font=('Arial', 20, 'normal'))
else:
    write("Danke fürs Spielen",font=('Arial', 20, 'normal'))
   
# Ende
exitonclick() 

Comments