Lektion 11 - Tic-Tac-Toe, Der Anfang 1

Erstellt von Frithjof Sun, 09 Sep 2007 21:53:00 GMT

Nach der Lektion 10 hatten wir eine kleine Pause eingelegt und in verschiedenen Theorie-Lektionen grundlegende Fähigkeiten beim Umgang mit der Programmiersprache Ruby erworben.

In dieser Lektion fangen wir, wie schon lange angekündigt, damit an, ein Spiel in Ruby zu entwickeln. Ich habe für dich das Spiel Tic-Tac-Toe (engl. Infos unter Tictactoe) ausgesucht. Die Regeln sind leicht zu verstehen, zumindest für dich und mich als Menschen. Ein Computer hat es da etwas schwerer. Wir werden es ihm aber auch beibringen.

Du hast also in den nächsten 7 oder mehr Lektionen einiges vor. Wir teilen uns die Arbeit etwa folgendermaßen ein:

  1. Das Spielbrett Ausgabe des Spielbretts mit den aktuell platzierten Spielsteinen.
  2. Eingabe von Zügen Zwei Spieler sollen nacheinander ihre Züge eingeben können.
  3. Der Computer als Gegner Du kannst alleine gegen den Computer spielen. Der Computer muss die Regeln des Spiels lernen.

Bevor wir in Ruby loslegen, schauen wir uns gemeinsam zunächst die Spielregeln von Tic-Tac-Toe an.

Kurze Spielanleitung

Das Spielbrett besteht aus 3 mal 3 Kästchen. Die zwei Spieler erhalten jeder eine Sorte Steine, die sie abwechselnd in die Kästchen setzen. Gewinner ist, wer zuerst 3 gleiche Steine in einer Reihe, Spalte oder Diagonale setzt. Du kannst das Spiel natürlich auch auf Papier mit Kreuzchen und Kreisen als Spielsteine spielen. Wenn beide Spieler optimal spielen, läuft Tic-Tac-Toe immer auf ein Unentschieden hinaus. Du kannst also nur gewinnen, wenn dein Gegner einen Fehler macht, egal wie du dich anstrengst. Umgekehrt gilt das natürlich auch.

Im folgenden Bild siehst du den Verlauf eines Spieles von links nach rechts und oben nach unten:

Spieler Kreis fängt an und setzt oben links in die Ecke. Spieler Kreuz hat 3 Möglichkeiten direkt neben ihn zu setzen und entscheidet sich für die Mitte, hätte aber auch sonst irgendwo seinen Stein setzen können. Dann ist Kreis wieder dran und versucht nun die erste Spalte mit seinen blauen Kreisen aufzufüllen. Das verhindert Kreuz im nächsten Zug durch seinen Stein unten links. Kreis gibt nicht auf und versucht nun die erste Zeile aufzufüllen mit seinem Stein oben rechts. Aber Kreuz passt auf und vermasselt ihm das auch durch seinen Stein in die Mitte oben. Auweia, Kreis sieht, dass Kreuz im nächsten Zug die zweite Spalte für sich holen könnte und es bleibt ihm nichts anderes übrig, als die Mitte unten zu besetzen. Jetzt ist das Spiel eigentlich schon gelaufen, die beiden freien Felder können von beiden beliebig gesetzt werden, keiner kann mehr gewinnen—Unentschieden!

Solltest du das Spiel noch nicht kennen, dann übe es zunächst ein paar mal mit Mama solange, bis du es ganz verstanden hast. Vielleicht hast du ja schon ein paar Ideen, wie du garantiert nie verlieren kannst, also höchstens mit Unentschieden das Spielfeld verlässt?

Das Spielbrett in Ruby

Okay, lass uns loslegen, das Spielfeld in Ruby aufzuzeichnen! Wir werden die 9 Felder des Spielfelds von links nach rechts und von oben nach unten fortlaufend durchnummerieren. Ist ein Feld leer, also hat noch niemand der beiden Spieler es besetzt, so geben wir die Nummer des Feldes aus. Die Nummer können wir dann später verwenden, um vom Spieler zu erfragen, in welches Feld er seinen Stein setzen möchte.

Ist ein Feld aber schon von einem Spieler besetzt, so geben wir im Feld das Zeichen für den Spieler aus. Wir legen fest: Spieler 1 hat ein O (der Buchstabe Ohh für den blauen Kreis) und Spieler 2 bekommt ein X (für das rote Kreuz).

Wenn wir das Spielfeld ausgeben wollen, müssen wir also die bereits gemachten Züge berücksichtigen. Wir müssen sie uns also irgendwo merken. Na klar, wir merken sie uns natürlich in einer Liste.


  # Variable für die Liste der Züge; Sie ist anfangs leer.
  zuege = []
Wir legen weiter fest, dass ein Zug aus 3 Teilen besteht:
  1. Welcher Spieler?
  2. Welche Spalte?
  3. Welche Zeile?

Somit können wir einen Zug wiederum als eine kleine Liste mit immer 3 Elementen speichern. Wir definieren dafür eine Methode, der wir diese 3 Elemente übergeben und zusätzlich noch die aktuelle Liste aller Züge. Die Methode fügt dann den neuen Zug der Liste hinzu. Das neue Element in der Zug-Liste ist also selbst eine kleine Liste!


  # Methode zum Hinzufügen eines Zuges.
  def zug_hinzu(wer, spalte, zeile, zuege)
    # Ein Zug besteht aus einer kleinen Liste mit genau 3 Elementen:
    # 1. Element 'wer': gibt den Spieler an, entweder :x oder :o
    # 2. Element 'spalte': die Spalte, in der der Zug gesetzt werden soll
    # 3. Element 'zeile': die Zeile, in die der Zug gesetzt werden soll
    zuege << [wer, spalte, zeile]
  end

Bis hierher dürfte es noch nicht allzu schwer gewesen sein, oder? Dafür wird es aber nun etwas knifflig. Wir brauchen eine Methode, die uns ein bestimmtes Feld ausgibt. Schauen wir sie uns zunächst an.


01  # Methode, die ein bestimmtes Feld ausgibt. Entweder wird
02  # das Symbol für den Spieler ausgegeben, der das Feld besetzt hat,
03  # oder es wird die laufende Nummer des Feldes ausgegeben.
04  def print_feld(spalte, zeile, zuege)
05    feld = (spalte-1)*1 + (zeile-1)*3 + 1
06    for zug in zuege do
07      if zug[1] == spalte and zug[2] == zeile
08        feld = (zug[0] == :x ? "X" : "O") 
09        break
10      end
11    end
12    print feld
13  end

Die Methode ist zwar kurz, aber es passiert hier eine Menge. Ich habe die Zeilen daher einmal nummeriert. Die Methode bekommt nun also die aktuelle Zeile und Spalte des Feldes, das sie ausgeben soll. Zusätzlich bekommt sie noch die Liste aller bisher gemachten Züge.

Die Methode muss nun folgendes machen:
  1. Die laufende Nummer für das Feld berechnen.
  2. Nachschauen, ob für das Feld bereits ein Zug existiert.
  3. Wenn ein Zug existiert, feststellen, welcher Spieler ihn gemacht hat und dann die laufende Nummer mit dem entsprechenden Zeichen für den Spieler überschreiben.

Schauen wir uns zuerst die Berechnung der laufenden Nummer von 1 bis 9 an. Das passiert in Zeile 05 in obigem Code.


...
05    feld = (spalte-1)*1 + (zeile-1)*3 + 1
...

Die laufende Nummer wird durch eine mathematische Formel aus der Spalte und der Zeile bestimmt. Erforderlich ist dabei, dass die Zeilen und Spalten jeweils von 1 bis 3 von links nach rechts bzw. von oben nach unten gezählt werden. Wenn du nicht glaubst, dass das funktioniert, dann probier einfach alle Möglichkeiten für die Formel aus. Zum Beispiel, welche Nummer muss in Spalte 3 und Zeile 2 stehen (das Feld in der Mitte ganz rechts außen)? Setzen wir die Werte in die Formel ein:


...
05    feld = (3-1)*1 + (2-1)*3 + 1
...

Wenn du das ausrechnest, erhälst du die gesuchte Zahl 6:


...
05    feld = 6
...

Weiter geht’s! Nachdem wir nun die Nummer vorsorglich schon mal berechnet haben, müssen wir nun doch noch schauen, ob nicht vielleicht schon ein Zug eines Spielers dieses Feld besetzt hat. Das machen wir in der for-Schleife in den Zeilen 06 bis 11.


...
06    for zug in zuege do
07      if zug[1] == spalte and zug[2] == zeile
08        feld = (zug[0] == :x ? "X" : "O") 
09        break
10      end
11    end
...

Hier gehen wir alle Züge durch, schauen, ob es einen Zug gibt, der für die aktuelle Spalte und Zeile zutrifft. Bedenke, dass ein Zug aus einer kleinen Liste mit 3 Elementen besteht. Das zweite Element ist die Spalte, die wir mit dem Index 1 erreichen, das dritte Element ist die Zeile, die wir mit dem Index 2 erreichen und jeweils mit den Werten der aktuell auszugebenden Spalte und Zeile vergleichen können:


...
07      if zug[1] == spalte and zug[2] == zeile
...

Wenn die IF-Abfrage true liefert, dann haben wir einen Zug gefunden, der auf das aktuelle Feld zutrifft. Dann schauen wir uns an, wer der beiden Spieler diesen Zug gemacht hat. Den Spieler merkten wir uns ja im ersten Element des Zuges, also beim Index 0.


...
08        feld = (zug[0] == :x ? "X" : "O") 
...

Hallo? Was bedeutet denn diese Zeile? Hier ist etwas neu, wie du sicher schon bemerkt hast. Wir verwenden hier den sogennanten dreiwertigen Vergleichsoperator (ternärer Operator). Den stellst du dir am einfachsten als eine IF-Abfrage in nur einer Zeile vor. Statt der Zeile oben hätten wir auch schreiben können:


  if zug[0] == :x
    feld = "X" 
  else
    feld = "O" 
  end

Beim dreiwertigen Vergleich ersetzt man also das vorangehende if durch ein nachgestelltes ? und das else durch einen : (Doppelpunkt). Das end fällt weg, weil man sowieso nur eine Zeile braucht.

Aber noch etwas ist neu! Was soll eigentlich :x bedeuten? Das nennt man in Ruby ein Symbol. Das ist wiederum eine Kurzschreibweise für eine Zeichenkette. Statt :x könnten wir auch "x" schreiben. Das richtige Gespühr, wann es sinnvoll ist ein Symbol und wann eine Zeichenkette zu verwenden, wirst du mit der Zeit noch bekommen.

Die Methode für das Ausgeben eines einzigen Feldes hat es also ganz schön in sich. Vielleicht musst du dir das alles in Ruhe erst noch einmal durchdenken.

Wir können nun ein Feld ausgeben. Eine Zeile wollen wir als nächstes ausgeben. Sie besteht immer aus 3 Feldern, die wir von links nach rechts ausgeben. Das macht folgende Methode:


  # Methode zum Ausgeben einer einzigen Zeile im Ausgabebereich 'out'.
  # Welche Zeile ausgegeben werden soll ist in 'zeile' übergeben.
  # Die Liste der Züge in 'zuege' brauchen wir hier, um das richtige
  # Symbol (X oder O) später in den Feldern ausgeben zu können, 
  # oder die Nummer des Feldes.
  def print_zeile(out, zeile, zuege)
    spalte = 1
    1.upto(3) do 
      print_feld(spalte, zeile, zuege)
      out.print " | " unless spalte == 3
      spalte += 1
    end
  end

Das sieht nun nicht mehr so schwierig aus. Wir rufen 3 mal die Methode für die Ausgabe eines Feldes auf. Die Zeile erhalten wir vom Aufruf, die Spalten zählen wir in der Methode selber von 1 bis 3 hoch. Nach jedem Feld geben wir einen senkrechten Strich aus, der den Rand des Kästchens markieren soll.

Wir haben es endlich geschafft. Hier ist die oberste Methode, die nun das gesamte Spielfeld ausgibt, indem sie alle Zeilen ausgibt:


  # Methode, die das Spielfeld im Ausgabebereich 'out' ausgibt
  # und dabei auch die in 'zuege' angegebenen Züge mit ausgibt.
  def spielfeld(out, zuege)
    out.puts  "/-----------\\" 
    out.print "| " 

    print_zeile(out, 1, zuege)

    out.puts " |" 
    out.puts  "|---|---|---|" 
    out.print "| " 

    print_zeile(out, 2, zuege)

    out.puts " |" 
    out.puts  "|---|---|---|" 
    out.print "| " 

    print_zeile(out, 3, zuege)

    out.puts " |" 
    out.puts "\\-----------/" 
  end

Du stimmst mir sicher zu, wenn ich sage, das reicht für heute!

Zum Schluß nochmal das gesamte Programm, das unser Spielfeld ausgibt. Zum Testen fügen wir einmal nach der ersten Ausgabe einen Zug in die Zug-Liste hinzu und geben das Spielfeld ein zweites mal aus. Die Variable STDOUT ist eine Konstante und ist der Name des Ausgabebereichs, in den wir ausgeben wollen. Die Standardausgabe (STDOUT) ist die Konsole, die wir bisher auch schon immer verwendet haben (nur hatten wir sie bisher nie bei ihrem richtigen Namen gerufen).


# lektion_11.rb

# Methode, die das Spielfeld im Ausgabebereich 'out' ausgibt
# und dabei auch die in 'zuege' angegebenen Züge mit ausgibt.
# Ist für ein Feld noch kein Zug erfolgt, dann wird die
# Nummer des Feldes ausgegeben. Die Felder sind dabei von
# links nach rechts und oben nach unten von 1 bis 9 fortlaufend
# nummeriert.
def spielfeld(out, zuege)
  out.puts  "/-----------\\" 
  out.print "| " 

  print_zeile(out, 1, zuege)

  out.puts " |" 
  out.puts  "|---|---|---|" 
  out.print "| " 

  print_zeile(out, 2, zuege)

  out.puts " |" 
  out.puts  "|---|---|---|" 
  out.print "| " 

  print_zeile(out, 3, zuege)

  out.puts " |" 
  out.puts "\\-----------/" 
end

# Methode zum Ausgeben einer einzigen Zeile im Ausgabebereich 'out'.
# Welche Zeile ausgegeben werden soll ist in 'zeile' übergeben.
# Die Liste der Züge in 'zuege' brauchen wir hier, um das richtige
# Symbol (X oder O) später in den Feldern ausgeben zu können, 
# oder die Nummer des Feldes.
def print_zeile(out, zeile, zuege)
  spalte = 1
  1.upto(3) do 
    print_feld(spalte, zeile, zuege)
    out.print " | " unless spalte == 3
    spalte += 1
  end
end

# Methode, die ein bestimmtes Feld ausgibt. Entweder wird
# das Symbol für den Spieler ausgegeben, der das Feld besetzt hat,
# oder es wird die laufende Nummer des Feldes ausgegeben.
def print_feld(spalte, zeile, zuege)
  res = (spalte-1)*1 + (zeile-1)*3 + 1
  for z in zuege do
    if z[1] == spalte and z[2] == zeile
      res = (z[0] == :x ? "X" : "O") 
      break
    end
  end
  print res
end

# Methode zum Hinzufügen eines Zuges.
def zug_hinzu(wer, spalte, zeile, zuege)
  # Ein Zug besteht aus einer kleinen Liste mit genau 3 Elementen:
  # 1. Element 'wer': gibt den Spieler an, entweder :x oder :o
  # 2. Element 'spalte': die Spalte, in der der Zug gesetzt werden soll
  # 3. Element 'zeile': die Zeile, in die der Zug gesetzt werden soll
  zuege << [wer, spalte, zeile]
end

# Variable für die Liste der Züge; Sie ist anfangs leer.
zuege = []

# Spielfeld auf der Standardausgabe einmal ausgeben.
spielfeld(STDOUT, zuege)

# Einen Zug zum Testen: O setzt oben links!
zug_hinzu(:o, 1, 1, zuege)

# Neues Spielfeld ausgeben
spielfeld(STDOUT, zuege)

Peter und Livia

Peter: Ich bin enttäuscht! Ich hatte mich auf ein Spiel mit Grafik und Maus und so gefreut. Stattdessen wieder nur das olle schwarze Fenster.

Livia: Das ist eigentlich keine schlechte Idee—für später. Aber meinst du nicht, dass das Spiel auf der Konsole erst einmal genug an Arbeit ist? Würden wir gleich mit Grafik und Mausbewegungen loslegen, hätten wir ja noch mehr zu tun. Das würde die Sache nicht leichter machen.

Trackbacks

Verwenden Sie den folgenden Link zur Rückverlinkung von Ihrer eigenen Seite:
http://www.rubykids.de/trackbacks?month=09&year=2007&article_id=tic-tac-toe-der-anfang&day=09

Meine Nachricht

Einen Kommentar hinterlassen

  1. Cpp over 4 years later:
    so ein mist, wo sind die interaktiven möglichkeiten?
Comments