Lektion 13 - Tic-Tac-Toe, Wer gewinnt?

Erstellt von Frithjof Sun, 30 Sep 2007 04:35:00 GMT

Weiter mit Tic-Tac-Toe. In dieser Lektion wirst du den Gewinner eines Spieles bestimmen. Das Spiel ist beendet, sobald einer der Spieler eine beliebige Reihe horizontal, vertikal oder diagonal mit seinen Steinen belegen konnte. Die Methode dafür könnte so arbeiten:
  • Sie bestimmt für alle möglichen Reihen (horizontal, vertikal, oder diagonal) die Anzahl der Steine des Spielers, der gerade den aktuellen Zug gemacht hat.
  • Sobald die Methode eine Reihe findet, in der der Spieler 3 Steine hat, hört sie auf zu suchen und gibt bekannt, dass dieser Spieler gewonnen hat.

Die Methode schreiben wir natürlich in die Datei tictactoe.rb, in der wir ja alle Methoden rund um das Spiel sammeln. Suche dort die bereits angelegte Methode ist_beendet?. Sie überprüft, ob das Spiel bereits zu Ende ist und sie musst du erweitern, denn das Spiel ist ebenfalls beendet, wenn einer der Spieler gewonnen hat. Den Test, ob und wer gewonnen hat machst du in einer separaten Methode:


# Berechnet die Feldnummer aus gegebener Spalte und Zeile
def nummer_aus_spalte_zeile(spalte, zeile)
  nummer = 0
  nummer = (spalte-1)*1 + (zeile-1)*3 + 1 (unless spalte.nil? or zeile.nil?)
  nummer
end

# Stellt fest, ob es einen Gewinner gibt
def the_winner_is(zuege)
end

# Schaut nach, ob das Spiel schon aus ist
def ist_beendet?(zuege)
  alle_zuege_gemacht = zuege.nil? ? false : zuege.size >= 9
  gewinner = the_winner_is(zuege)
  alle_zuege_gemacht or gewinner != nil
end

Du fügst in der Methode ist_beendet? eine Variable alle_zuege_gemacht ein. Zuerst testet die Methode, ob das Spiel beendet ist, weil alle Züge gemacht wurden. Dann bestimmt sie den Gewinner durch den Aufruf der Methode the_winner_is. Das Spiel ist nun beendet, wenn entweder alle möglichen Züge gemacht wurden, oder es einen Gewinner gibt.

Die Felder überprüfen wir am besten über die Feldnummer. Dafür brauchst du die Methode nummer_aus_spalte_zeile, die aus einer Spalte und Zeile die zugehörige Feldnummer zurückberechnet. Bei der Eingabe von Zügen machst du ja genau das Umgekehrte: die Berechnung von Spalte und Zeile aus der eingetippten Feldnummer.

Weiter mit der Methode the_winner_is. Es geht los mit den Reihen. Du legst für jede Reihe eine kleine Liste mit 3 Elementen für die 3 Felder der Reihe an. Alle Reihen packst du in eine große gemeinsame Liste. Du legst eine Variable für den möglichen Gewinner an und gibst ihn schon mal als Ergebnis zurück.


def the_winner_is(zuege)
  reihen = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],

    [1, 4, 7],
    [2, 5, 8],
    [3, 6, 9],

    [1, 5, 9],
    [3, 5, 7],
  ]

  # Variable für den Gewinner
  the_winner = nil

  the_winner
end

Naja, das ist noch nicht viel, aber immerhin würde das Programm so schon fehlerfrei durchlaufen. Es erkennt den Gewinner aber natürlich noch nicht. Also weiter geht’s.


def the_winner_is(zuege)
  reihen = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], 
             [1, 4, 7], [2, 5, 8], [3, 6, 9], 
             [1, 5, 9], [3, 5, 7], ]

  # Variable für den Gewinner
  the_winner = nil

  # Für beide Spieler testen
  for spieler in [:o, :x]
    felder_besetzt = []

    for zug in zuege
      if zug[0] == spieler
        feld = nummer_aus_spalte_zeile(zug[1], zug[2])
        felder_besetzt << feld
      end
    end

  end

  the_winner
end

In einer Schleife betrachtest du beide Spieler nacheinander. Du legst für den Spieler eine Liste an, felder_besetzt. In der merkst du dir genau die Felder, die der aktuelle Spieler bereits besetzt hält.

Anschließend gehst du Reihe für Reihe durch und schaust, ob für eine Reihe die Liste felder_besetzt alle Felder der Reihe enthält. Das wäre dann die Reihe, mit der der Spieler gewonnen hat. Falls keine der Reihen alle ihre Felder in der Liste felder_besetzt wiederfindet, dann hat der Spieler noch nicht gewonnen.


def the_winner_is(zuege)
  reihen = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], 
             [1, 4, 7], [2, 5, 8], [3, 6, 9], 
             [1, 5, 9], [3, 5, 7], ]

  # Variable für den Gewinner
  the_winner = nil

  # Für beide Spieler testen
  for spieler in [:o, :x]
    felder_besetzt = []

    for zug in zuege
      if zug[0] == spieler
        feld = nummer_aus_spalte_zeile(zug[1], zug[2])
        felder_besetzt << feld
      end
    end

    # In felder_besetzt stehen die Felder, die vom aktuellen
    # Spieler belegt sind. Die können wir nun für alle Reihen testen.
    for reihe in reihen
      gewonnen = true
      for feld in reihe
        # gewonnen wird falsch (false), wenn das aktuelle Feld
        # der Reihe nicht besetzt ist.
        gewonnen = (gewonnen and felder_besetzt.include?(feld))
        break if gewonnen == false # in der Reihe kein Gewinn mehr 
      end
      if gewonnen
        the_winner = spieler
        break # Gewinner gefunden, aufhören weiter zu suchen
      end
    end

    # Wenn es einen Gewinner gibt, für den nächsten gar nicht 
    # erst mehr versuchen, denn dieser kann nicht auch gleichzeitig 
    # gewonnen haben, das hätten wir beim vorherigen Zug bereits bemerkt.
    break if the_winner != nil

  end

  the_winner
end

Zwei Dinge sollten wir uns hier etwas genauer anschauen, die in der einen Zeile


  ...
  gewonnen = (gewonnen and felder_besetzt.include?(feld))
  ...

passieren. Die Frage-Nachricht include? wird hier an die Liste felder_besetzt geschickt. Include? bedeutet soviel wie Enthältst du das hier?. Was die Liste felder_besetzt enthalten soll, geben wir der Nachricht in der Variable feld mit. Die Frage-Nachricht liefert uns ein true oder false zurück, je nachdem ob die Liste das angefragte Feld enthält oder nicht.

Das zweite was in dieser Zeile passiert, obwohl nicht so offensichtlich, ist folgendes. Die Variable gewonnen hast du zu Anfang auf true gesetzt. Gleichzeitig verwendest du sie hier in dieser Zeile aber wieder für die Zuweisung für sich selbst. Somit kann die Variable gewonnen das erste mal nur dann falsch (false) werden, wenn der zweite Teil der Zuweisung (also die Frage ob das Feld vom Spieler besetzt ist) nach dem and falsch ist. Es gibt also folgende Möglichkeite in dieser Zeile:
  1. gewonnen = wahr und Spieler hat das Feld besetzt, dann bleibt die Variable gewonnen auf wahr stehen.
  2. gewonnen = wahr und Spieler hat das Feld nicht besetzt, dann wird die Variable gewonnen das erste mal falsch.
Sobald die Variable gewonnen aber einmal falsch geworden ist, kann sie nie wieder für die aktuell betrachtete Reihe wahr werden und es lohnt nicht, die verbleibenden Felder der Reihe zu testen (break). Denn dann sehen die Möglichkeiten so aus:
  1. gewonnen = falsch und Spieler hat das Feld besetzt, dann bleibt die Variable gewonnen auf falsch stehen, da nicht beide Bedingungen zugleich wahr sind.
  2. gewonnen = falsch und Spieler hat das Feld nicht besetzt, dann bleibt die Variable gewonnen genauso auf falsch stehen.

Mit diesem Trick kannst du somit ganz leicht bestimmen, ob alle Felder einer Reihe tatsächlich besetzt sind.

Prima, wir sind fast fertig. Das Programm bricht nun ab, sobald alle möglichen Züge gemacht sind, oder es einen Gewinner gibt. Aber wir haben ein Problem. Wie können wir nun den Gewinner an der Konsole ausgeben? Die Methode ist_beendet? gibt uns ja nur ein true oder false zurück, den Gewinner selbst nicht.

Schau dir die Methode play_2_spieler an. Dort rufst du die Methode ist_beendet? auf. Am Ende von play_2_spieler wird der Spieler zurück gegeben, der den letzten gültigen Zug gemacht hat. Diesen Spieler könntest du in dem Hauptprogramm am Ende einfach ausgeben. Aber das reicht noch nicht. Du kannst nicht unterscheiden, warum das Spiel zu Ende ist.


# Lässt 2 Spieler miteinander spielen
def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  while true
    ...
    break if (ist_beendet?(zuege) or !zug_okay)
    wer += 1
    wer %= 2
  end
  spieler[wer]
end

Die Methode ist_beendet? ist bisher die einzige Methode, die den möglichen Gewinner kennt. Also, dann lass sie uns dazu bewegen, diesen Gewinner nicht länger geheim zu halten.


# Schaut nach, ob das Spiel schon aus ist
def ist_beendet?(zuege)
  alle_zuege_gemacht = zuege.nil? ? false : zuege.size >= 9
  gewinner = the_winner_is(zuege)
  # Zwei Rückgabewerte in einer Liste:
  # Erster Wert: gibt an (true, false), ob das Spiel aus ist
  # Zweiter Wert: der Gewinner (oder nil, falls es keinen gibt)
  [(alle_zuege_gemacht or (gewinner != nil)), gewinner]
end

# Lässt 2 Spieler miteinander spielen
def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  ergebnis = [false, nil]
  while true
    zug_okay = false
    until zug_okay
      out.print "#{spieler[wer][1]} ist am Zug: " 
      nummer = ein.gets.to_i
      break if nummer == 0
      spalte, zeile = nummer_in_spalte_zeile(nummer)
      zug_okay = zug_hinzu(spieler[wer][0], spalte, zeile, zuege)
    end
    spielfeld(out, zuege)
    ergebnis = ist_beendet?(zuege)
    beendet = ergebnis[0]
    break if (beendet or !zug_okay)
    wer += 1
    wer %= 2
  end
  # Rückgabewerte: der aktuelle Spieler, falls er der Gewinner ist.
  gewinner = ergebnis[1]
  if gewinner != nil
    return spieler[wer]
  else
    return nil
  end
end

Die Methode ist_beendet? gibt nun zwei Werte in einer kleinen Liste zurück: als erstes wahr oder falsch, wenn das Spiel beendet ist und als zweites den Gewinner, falls es einen gibt, oder nil, falls es keinen Gewinner gibt.

Die Methode play_2_spieler berücksichtigt nun diesen neuen Rückgabewert, holt sich aus dem ersten Element der Liste die Information, ob das Spiel aus ist und gibt selbst nicht mehr immer einen Spieler zurück, sondern nur noch, wenn dieser ein Gewinner ist. Gibt es keinen Gewinner, gibt sie nil zurück.

Nun kannst du das Hauptprogramm in lektion_13.rb anpassen, damit schließlich dort die richtigen Ausgaben gemacht werden.


# lektion_13.rb

require File.dirname(__FILE__) +  "/tictactoe" 

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

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

gewinner = play_2_spieler(STDOUT, STDIN, zuege)

# Gibt es einen Gewinner?
if gewinner == nil
  puts "Das Spiel endet UNENTSCHIEDEN!" 
else
  puts "Der Gewinner ist #{gewinner[1]}!" 
end

Hier nochmal die gesamte Datei tictactoe.rb:


# Copyright (C) 2007 www.rubykids.de
# tictactoe.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)
  # Nicht erlauben, wenn das Feld schon besetzt ist
  erlaubt = true
  zuege.each do |zug|
    if zug[1] == spalte and zug[2] == zeile
      # Einen Zug für diese Feld gibt es schon
      erlaubt = false
      break
    end
  end
  # 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] if erlaubt
  erlaubt
end

# Bestimmt aus der Nummer eines Feldes die Spalte und Zeile
# Angenommen Spalte und Zeilen würden von 0 bis 2 gezählt werden.
# Dann ergeben sich folgende Formeln:

# Spalte, Zeile => Nummer => Formel
# ----------------------------------------
# 0,0           => 1      => 0*1 + 0*3 + 1
# 1,0           => 2      => 1*1 + 0*3 + 1
# 2,0           => 3      => 2*1 + 0*3 + 1
# 0,1           => 4      => 0*1 + 1*3 + 1
# 1,1           => 5      => 1*1 + 1*3 + 1
# 2,1           => 6      => 2*1 + 1*3 + 1
# 0,2           => 7      => 0*1 + 2*3 + 1
# 1,2           => 8      => 1*1 + 2*3 + 1
# 2,2           => 9      => 2*1 + 2*3 + 1
def nummer_in_spalte_zeile(num)
  spalte = ((num-1) % 3)
  zeile = (((num + 2 ) / 3 ) - 1)
  [spalte+1, zeile+1]
end

# Berechnet die Feldnummer aus gegebener Spalte und Zeile
# Tabelle für Zuordnung siehe oben bei Methode nummer_in_spalte_zeile.
def nummer_aus_spalte_zeile(spalte, zeile)
  nummer = 0
  nummer = (spalte-1)*1 + (zeile-1)*3 + 1 unless (spalte.nil? or zeile.nil?)
  nummer
end

# Stellt fest, ob es einen Gewinner gibt
def the_winner_is(zuege)
  reihen = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],

    [1, 4, 7],
    [2, 5, 8],
    [3, 6, 9],

    [1, 5, 9],
    [3, 5, 7],
  ]

  # Variable für den Gewinner
  the_winner = nil

  # Für beide Spieler testen
  for spieler in [:o, :x]
    felder_besetzt = []

    for zug in zuege
      if zug[0] == spieler
        feld = nummer_aus_spalte_zeile(zug[1], zug[2])
        felder_besetzt << feld
      end
    end

    # In felder_besetzt stehen die Felder, die vom aktuellen Spieler 
    # belegt sind. Die können wir nun für alle Reihen testen.
    for reihe in reihen
      gewonnen = true
      for feld in reihe
        # gewonnen wird falsch (false), wenn das aktuelle Feld der 
        # Reihe nicht besetzt ist.
        gewonnen = (gewonnen and felder_besetzt.include?(feld))
        break if gewonnen == false # in der Reihe kein Gewinn mehr 
      end
      if gewonnen
        the_winner = spieler
        break # Gewinner gefunden, aufhören weiter zu suchen
      end
    end

    # Wenn es einen Gewinner gibt, für den nächsten gar nicht erst 
    # mehr versuchen, denn dieser kann nicht auch gleichzeitig 
    # gewonnen haben, das hätten wir beim vorherigen Zug bereits 
    # bemerkt.
    break if the_winner != nil
  end

  the_winner
end

# Schaut nach, ob das Spiel schon aus ist
def ist_beendet?(zuege)
  alle_zuege_gemacht = zuege.nil? ? false : zuege.size >= 9
  gewinner = the_winner_is(zuege)
  # Zwei Rückgabewerte in einer Liste:
  # Erster Wert: gibt an (true, false), ob das Spiel aus ist
  # Zweiter Wert: der Gewinner (oder nil, falls es keinen gibt)
  [(alle_zuege_gemacht or (gewinner != nil)), gewinner]
end

# Lässt 2 Spieler miteinander spielen
def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  ergebnis = [false, nil]
  while true
    zug_okay = false
    until zug_okay
      out.print "#{spieler[wer][1]} ist am Zug: " 
      nummer = ein.gets.to_i
      break if nummer == 0
      spalte, zeile = nummer_in_spalte_zeile(nummer)
      zug_okay = zug_hinzu(spieler[wer][0], spalte, zeile, zuege)
    end
    spielfeld(out, zuege)
    ergebnis = ist_beendet?(zuege)
    beendet = ergebnis[0]
    break if (beendet or !zug_okay)
    wer += 1
    wer %= 2
  end
  # Rückgabewerte: der aktuelle Spieler, falls er der Gewinner ist.
  gewinner = ergebnis[1]
  if gewinner != nil
    return spieler[wer]
  else
    return nil
  end
end

Peter und Livia

Peter: Mir ist die erste Codezeile in der Methode ist_beendet? aufgefallen:

def ist_beendet?(zuege)
  alle_zuege_gemacht = zuege.nil? ? false : zuege.size >= 9
  ...
end
Was bedeuten die beiden Fragezeichen so direkt nacheinander?

Livia: Das erste Fragezeichen gehört zur Nachricht nil?, die an die Variable zuege geschickt wird. Sie fragt die Variable danach, ob sie nil ist. Falls sie das ist liefert die Nachricht true zurück. Falls die Variable aber einen echten Wert besitzt, dann liefert sie false. Das zweite Fragezeichen stammt vom ternären Operator, der ja eine abgekürzte Schreibweise für eine IF-Abfrage darstellt.

Trackbacks

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

Meine Nachricht

Einen Kommentar hinterlassen

Comments