Lektion 17 - Tic-Tac-Toe objektorientiert

Erstellt von Frithjof Fri, 08 Feb 2008 22:19:00 GMT

Dies ist die letzte Lektion zu Tic-Tac-Toe. Das Spiel wird einen nicht unerheblichen Umbau erfahren: es wird objektorientiert werden. Das heißt aber nicht, dass es danach optimal und fertig ist. Software ist nie fertig (und meistens leider auch nicht immer optimal). Es gibt stets noch etwas zu verbessern oder neue Funktionalität, die man noch hinzufügen könnte. In den nächsten Lektionen warten aber noch mehr Abenteuer mit Ruby auf dich.

In der Theorie Lektion 7 hast du eine Vorgehensweise kennengelernt, wie man aus einem beschreibenden Text, die notwendigen Objekte und Methoden herausfinden kann. Nehmen wir eine Spielbeschreibung (hier die etwas umformulierte aus der ersten Lektion zu Tic-Tac-Toe) her und versuchen zunächst nur die Objekte (bzw. Klassen) daraus zu bestimmen.

Das Spielfeld des Spiels Tic-Tac-Toe besteht aus 3 mal 3 Feldern. Zwei Spieler setzen bei jedem Zug abwechselnd einen Spielstein, um als erster 3 gleiche in einer horizontalen, vertikalen oder diagonalen Reihe zu haben.
Die Substantive sind die zu implementierenden Klassen:
  1. Spielfeld
  2. Spiel Tic-Tac-Toe
  3. Feld
  4. Spieler
  5. Zug
  6. Spielstein
  7. Reihe

Das ist zumindest schon mal ein Anfang. Vielleicht brauchen wir nicht alle Klassen, vielleicht aber auch ganz andere, die sich aus der Spielbeschreibung so nicht ablesen lassen.

Die Klasse Spieler

Bisher hatten wir einen Spieler als Liste mit zwei Elementen verwaltet. Das erste Element war ein Symbol zur Unterscheidung der Spieler, das zweite Element war das Zeichen für die Ausgabe der von diesem Spieler besetzten Felder auf dem Spielfeld.


  spieler = [[:o, 'O'], [:x, 'X']]

Ein Spieler hat somit mindestens ein Attribut für den Namen zur Unterscheidung. Die Klasse Spieler könnte daher am einfachsten so aussehen:


class Spieler
  attr_accessor :name

  def to_s
    @name.to_s.upcase unless @name.nil?
  end
end

hätte so aber den Nachteil, dass man nachträglich den Namen verändern könnte. Das wollen wir verhindern und schränken den Zugriff auf das Attribut name ein, damit es nur beim Anlegen eines Objektes festgelegt und danach nur noch gelesen werden kann.


class Spieler
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def to_s
    @name.to_s.upcase unless @name.nil?
  end
end

Wie testen die Klasse auch gleich mit folgendem Programmschnipsel:


s1 = Spieler.new(:x)
s2 = Spieler.new(:o)
s3 = Spieler.new("x")
s4 = Spieler.new("O")

puts s1, s2, s3, s4

C:\entwicklung>ruby lektion_17.rb
X
O
X
O

Prima! Weiter mit der nächsten Klasse.

Die Klasse Zug

Einen Zug im Spiel Tic-Tac-Toe hatten wir bisher als Liste mit den drei Elementen Spieler, Spalte und Zeile dargestellt. Der Zug Spieler O setzt in die Mitte sah zum Beispiel so aus:


  [:o, 2, 2]

Die Klasse für den objektorientierte Zug braucht somit mindestens ein Attribut für den Spieler (der den Zug macht). Dann braucht es noch Attribute, die die Position des Zuges auf dem Spielfeld festhalten. Die Position ist entweder durch die Angabe von Spalte und Zeile des Feldes eindeutig festgelegt, oder durch die Nummer des Feldes. Was nehmen wir?

Erinnern wir uns kurz daran, warum wir im bisherigen Code immer beides verwendet haben. Die Feldnummer erleichtert die Eingabe für den Spieler, der vor dem Computer sitzt. Er braucht nur eine Zahl von 1 bis 9 einzugeben.

Die Ausgabe des Spielfeldes erfordert jedoch die Darstellung als Spalte und Zeile, da sie ja zeilenweise erfolgt. Daher mussten wir aus der eingegebenen Feldnummer zunächst Spalte und Zeile berechnen. Das Umwandeln der Feldnummer in Spalte und Zeile und zurück übernahmen die beiden Methoden nummer_aus_spalte_zeile (wird 5 mal verwendet) und nummer_in_spalte_zeile (wird 9 mal verwendet).

Die Klasse Zug muss also folgendes leisten:
  1. Objekte lassen sich mit Angabe von Spieler und Feldnummer erzeugen
  2. Auf Wunsch kann ein Objekt aber auch die Spalte und Zeile liefern
  3. Das Umwandeln von Feldnummer in Spalte und Zeile und umbekehrt erledigt sie selbst; das bleibt für die Außenwelt unsichtbar.

Hier ist die Klasse Zug:


class Zug
  attr_reader :spieler, :nummer, :spalte, :zeile

  def initialize(spieler, nummer)
    @spieler = spieler
    @nummer  = nummer
    @spalte, @zeile = nummer_in_spalte_zeile(nummer)
  end

  def to_s
    "Zug[spieler=#{@spieler},nummer=#{@nummer},spalte=#{@spalte},zeile=#{@zeile}]" 
  end

  private

  def nummer_in_spalte_zeile(num)
    spalte = ((num-1) % 3)
    zeile = (((num + 2 ) / 3 ) - 1)
    [spalte+1, zeile+1]
  end

  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
end

Zwei Dinge sind neu, die wir hier kurz besprechen. Als erstes fällt das Schlüsselwort private auf. Das bedeutet, dass alle nachfolgenden Methoden der Klasse für die Außenwelt unsichtbar sind, sie können also nicht von außen aufgerufen werden. Nur innerhalb der Klasse sind sie verfügbar.

Zum Testen wieder ein kleines Beispiel:


s = Spieler.new(:o)
z = Zug.new(s, 7)

puts z
print "Nummer ist: ", z.nummer, "\n" 
print "Spalte ist: ", z.spalte, "\n" 
print "Zeile  ist: ", z.zeile, "\n" 

Das liefert:


C:\entwicklung>ruby lektion_17.rb
Zug[spieler=O,nummer=7,spalte=1,zeile=3]
Nummer ist: 7
Spalte ist: 1
Zeile  ist: 3

Die Klasse Spielfeld

Die Klasse Spielfeld kümmert sich um die Verwaltung der Züge und die Ausgabe des Spielfeldes. Beginnend mit der alten Methode spielfeld, die wir in print umbenennen, bauen wir nacheinander die anderen alten Methoden print_zeile und print_feld ein. Aber schauen wir uns die Klasse zunächst an.


class Spielfeld
  attr_accessor :feldnummerierung
  @@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],
  ]

  def initialize(feldnummerierung = false)
    @zuege = []
    @spieler_x = Spieler.new(:x)
    @spieler_o = Spieler.new(:o)
    @feldnummerierung = feldnummerierung
  end

  def spieler
    [@spieler_o, @spieler_x]
  end

  def zuege
    @zuege
  end

  def zug_hinzu(zug_neu)
    # Nicht erlauben, wenn das Feld schon besetzt ist
    erlaubt = true
    @zuege.each do |zug|
      if zug.nummer == zug_neu.nummer
        # Einen Zug für diese Feld gibt es schon
        erlaubt = false
        break
      end
    end
    @zuege << Zug.new(zug_neu.spieler, zug_neu.nummer) if erlaubt
    erlaubt
  end

  # Methode, die das Spielfeld im Ausgabebereich 'aus' 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 print(aus)
    aus.puts  "/-----------\\" 
    aus.print "| " 

    print_zeile(aus, 1)

    aus.puts " |" 
    aus.puts  "|---|---|---|" 
    aus.print "| " 

    print_zeile(aus, 2)

    aus.puts " |" 
    aus.puts  "|---|---|---|" 
    aus.print "| " 

    print_zeile(aus, 3)

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

  # Bestimmt den Status einer Reihe in der aktuellen Spielsituation. 
  # Rückgabewerte sind eine Liste der besetzten und der freien Felder.
  # Die Liste der besetzten Felder ist aufgeteilt nach Spielern und
  # in einem Hash nach folgender Form organisiert:
  #
  #  besetzt = {
  #    :o => Liste der von O besetzten Felder, 
  #    :x => Liste der von X besetzten Felder
  #  }
  #   
  def reihen_status(reihe)
    # Welche Felder sind noch frei?
    frei_alle = freie_felder
    frei = []
    for feld in reihe
      if frei_alle.include?(feld)
        frei << feld
      end
    end

    # Welche Felder sind vom wem besetzt? Da ist etwas mehr zu tun.
    besetzt = {}
    for s in [@spieler_o, @spieler_x]
      besetzt[s] = []
    end
    for zug in @zuege
      # Liegt der zug in der fraglichen Reihe?
      feld = zug.nummer
      if reihe.include?(feld)
        besetzt[zug.spieler] << feld
      end
    end
    [besetzt, frei]
  end

  def freie_felder
    frei = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    for zug in @zuege
      frei.delete(zug.nummer)
    end
    frei
  end

  # Schaut nach, ob alle Züge gemacht sind
  def felder_frei?
    @zuege.nil? ? true : @zuege.size < 9
  end

  def Spielfeld.reihen
    @@reihen
  end

  private

  # Methode zum Ausgeben einer einzigen Zeile im Ausgabebereich 'aus'.
  # Welche Zeile ausgegeben werden soll ist in 'zeile' übergeben.
  def print_zeile(aus, zeile)
    spalte = 1
    1.upto(3) do 
      print_feld(aus, spalte, zeile)
      aus.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, sofern
  # die Feldnummerierung angeschaltet ist.
  def print_feld(aus, spalte, zeile)
    res = " " 
    res = ((spalte-1)*1 + (zeile-1)*3 + 1) if @feldnummerierung
    # Den Zug suchen, der an dieser Stelle auszugeben ist.
    for zug in @zuege do
      if zug.spalte == spalte and zug.zeile == zeile
        res = zug.spieler
        break
      end
    end
    aus.print res
  end

end
Direkt nach der Definition der Klasse Spielfeld folgt eine Variable reihen, die eine Liste enthält, die wiederum aus acht kleinen Listen mit den drei Feldnummern der jeweiligen Reihe besteht. Das doppelte @-Zeichen am Beginn des Variablennamens @@reihen markiert diese Variable als Klassenvariable. Das bedeutet, dass es sie für alle Objekte die im Programmverlauf von dieser Klasse erzeugt werden nur ein einziges mal gibt. Es bekommt also nicht jedes Objekt der Klasse seine eigene Variable reihen. Das macht Sinn, denn jedes Spielfeld in Tic-Tac-Toe hat diese Reihen.

Im Konstruktor erzeugt die Klasse zunächst eine leere Liste für die Züge und legt die zwei Spieler X und O an.

Die Methode spieler liefert das Spielerpaar in einer kleinen Liste zurück.

Die Methode zuege, schon klar, sie gibt Zugriff auf die Liste mit den Zügen. Sie würde allerdings sicher bei der Kapselungskontrolle durchfallen. Aus Bequemlichkeit belassen wir es einmal dabei, behalten aber im Hinterkopf, dass hier jemand von außen die Züge manipulieren und so einen ungültigen Zug unterschieben könnte.

Die Methode zug_hinzu ist bekannt. Vergleiche sie mit der alten Methode! Es gibt nur noch einen Übergabeparameter: den neuen Zug als Objekt der Klasse Züge. Der Spieler, der diesen Zug ausführt ist im Objekt des Zuges selbst versteckt und die Spalte und Zeile und die gesamte Zugliste brauchen wir auch nicht mehr mit uns herumzuschleppen.

Die nächste Methode print gibt das Spielfeld aus.

Die Methode reihen_status liefert alle notwendige Zustandsinformation über eine Reihe. Beim Vergleich mit dem alten Code stellt sich heraus, dass wir damals diesen Reihenstatus vornehmlich für den intelligenten Computerzug verwendeten. Beim Umgestalten des Codes wird aber deutlich, dass wir bei der Bestimmung des Gewinners im Grunde auch einen Status über die Reihen erstellen. Somit können wir den Reihenstatus nun an mindestens zwei Stellen gut gebrauchen und die Bestimmung des Gewinners (kommt noch weiter unten) wird dadurch um ein paar Zeilen kürzer.

Wir haben hier während der Umbauarbeiten somit zwei Stellen im alten Code entdeckt, die dasselbe auf ziemlich ähnliche Weisen tun. Das ist der Smell Doppelter Code und das Umbauen des Codes nennt man Refactoring.

Weitere Methoden folgen, die auf die übliche Weise aus alten Methoden umgebaut oder neu eingefügt wurden. Besonders möchte ich hier auf die Methode Spielfeld.reihen verweisen. Das vorgefügte Spielfeld. vor den eigentlichen Methodennamen kennzeichnet eine sogenannte Klassenmethode. Genauso wie die Klassenvariable, die wir oben schon kennen gelernt haben, gibt es diese Methode für alle Objekte der Klasse nur einmal. Die Methode macht nichts weiter als nach außen Zugriff auf die Liste mit den Reihen zu gewähren.

Durch unser Refactoring haben wir nun alle Methoden, die irgendwie etwas mit dem Spielfeld zu tun haben in der Klasse Spielfeld zusammen gruppiert, wir haben doppelten Code zusammengefasst und – oops sogar einen Fehler, der bisher unbemerkt bliebt, gefunden. Schau dir den alten Code nochmal an. Die Methode spielfeld wird mit dem Ausgabebereich aufgerufen (Parameter out), den sie bei jeder Zeilenausgabe an die Methode print_zeile weiterreicht. Bei der Zeilenausgabe ruft print_zeile für jedes Feld die Methode print_feld auf und vergisst dabei aber den Ausgabebereich out weiterzureichen. Gute Gelegenheit das zu korrigieren.

Die Klasse TicTacToe verwaltet das Spielfeld und die Spielstrategie

Die Klasse TicTacToe verwaltet das Spielfeld und kümmert sich darum, vom menschlichen Spieler die Eingaben zu erfragen bzw. die Züge des Computergegners zu berechnen. Schauen wir uns die Klasse wieder zunächst an.


class TicTacToe
  attr_accessor :strategie

  def initialize(strategie = nil)
    @spielfeld = Spielfeld.new
    @strategie = strategie.nil? ? LeichteSpielStrategie.new : strategie
  end

  def feldnummerierung_ein
    @spielfeld.feldnummerierung = true
  end

  def feldnummerierung_aus
    @spielfeld.feldnummerierung = false
  end

  def play(aus, ein)
    @spielfeld.print(aus)
    gewinner = the_winner_is
    beendet  = !@spielfeld.felder_frei? or (gewinner != nil)
    spieler = @spielfeld.spieler

    wer = nil
    aus.print "Was spielst du, X oder O? " 
    eingabe = ein.gets.downcase.chomp
    wer = (eingabe == 'x') ? 1 : 0

    aus.print "Los geht's! Du spielst #{spieler[wer]}, und ich #{spieler[1^wer]}!" 
    aus.puts " Du faengst an." 

    while true
      # Der menschliche Spieler zuerst
      zug_okay = false
      until zug_okay
        aus.print "#{spieler[wer]} ist am Zug: " 
        nummer = ein.gets.to_i
        break if nummer == 0
        zug_okay = @spielfeld.zug_hinzu(Zug.new(spieler[wer], nummer))
      end
      @spielfeld.print(aus)
      gewinner = the_winner_is
      beendet  = ((!@spielfeld.felder_frei?) or (gewinner != nil))
      break if (beendet or !zug_okay)
      wer += 1
      wer %= 2

      # Gleich den Zug des Computers anschließen
      zug_okay = false
      until zug_okay
        aus.print "\nJetzt bin ich dran! " 
        zug = computer_zug(spieler[wer])
        break if zug.nil?
        aus.puts "Ich setze auf das Feld #{zug.nummer}." 
        zug_okay = @spielfeld.zug_hinzu(zug)
      end
      @spielfeld.print(aus)
      gewinner = the_winner_is
      beendet  = ((!@spielfeld.felder_frei?) or (gewinner != nil))

      break if (beendet or !zug_okay)
      wer += 1
      wer %= 2
    end
    # Rückgabewerte: der aktuelle Spieler, falls er der Gewinner ist.
    if gewinner != nil
      return spieler[wer]
    else
      return nil
    end

  end

  # Lässt 2 Spieler miteinander spielen
  def play_2_spieler(aus, ein)
    @spielfeld.print(aus)

    gewinner = the_winner_is
    spieler = @spielfeld.spieler
    gewinner = nil
    wer = 0
    while true
      zug_okay = false
      until zug_okay
        aus.print "#{spieler[wer]} ist am Zug: " 
        nummer = ein.gets.to_i
        break if nummer == 0
        zug_okay = @spielfeld.zug_hinzu(Zug.new(spieler[wer], nummer))
      end
      @spielfeld.print(aus)
      gewinner = the_winner_is
      beendet  = ((!@spielfeld.felder_frei?) or (gewinner != nil))
      break if (beendet or !zug_okay)
      wer += 1
      wer %= 2
    end
    # Rückgabewerte: der aktuelle Spieler, falls er der Gewinner ist.
    if gewinner != nil
      return spieler[wer]
    else
      return nil
    end
  end

  def computer_zug(aktueller_spieler)
    @strategie.next(@spielfeld, aktueller_spieler)
  end

  private

  # Stellt fest, ob es einen Gewinner gibt
  def the_winner_is
    # Variable für den Gewinner
    the_winner = nil

    # Für alle Reihen testen
    for reihe in Spielfeld::reihen
      besetzt, frei = @spielfeld.reihen_status(reihe)

      # In der Reihe nur ein Gewinn, wenn kein Feld mehr frei
      if frei.size == 0
        for spieler in @spielfeld.spieler
          # spieler hat in dieser Reihe gewonnen, wenn er alle
          # 3 möglichen Felder besetzt hat
          the_winner = spieler if besetzt[spieler].size == 3
          break if the_winner != nil # Gewinner gefunden, for-spieler-Schleife verlassen
        end
      end

      # Wenn es einen Gewinner gibt, ist es nicht mehr notwendig,
      # in den anderen Reihen nachzuschauen.
      break if the_winner != nil # for-reihe-Schleife verlassen
    end

    the_winner
  end
end

Den Konstruktor (Methode initialize) erzeugt zunächst ein leeres Spielfeld und legt die Spielstrategie fest. Sofern beim Erzeugen des Spiels keine Spielstrategie über den Parameter gewünscht wurde, verwendet die Klasse selbständig die leichte Spielstrategie. Die Klasse Strategie, die wir weiter unten besprechen, fasst die Methoden zusammen, die mit der Bestimmung von automatischen Zügen des Computergegners zu tun haben.

Das Spiel startet man von außen über eine der beiden Methoden play oder play_2_spieler. Die erste Methode play lässt das Spiel im Computermodus ablaufen, wobei ein menschlicher Spieler gegen den Computergegner spielen kann. Die Methode play_2_spieler führt zwei menschliche Spieler durch ein gemeinsames Spiel.

Auch in diesen Methoden haben wir einiges umzustellen gehabt – vergleiche selbst mit dem alten Code.

Schließlich bestimmt die private Methode the_winner_is den Gewinner.

Spielstrategien

Wir haben einiges geleistet und sind auch fast fertig. Schauen wir uns nur noch die Klassen der Spielstrategien an. Es gibt drei davon: SpielStrategie, LeichteSpielStrategie und SchwereSpielStrategie. Sie umfassen alle unsere bisherigen Methoden für die Bestimmung des nächsten Computerzuges. Die Methode intelligenter_zug habe ich dabei noch etwas anpassen müssen, weil Livia es doch tatsächlich schaffte, den Computer mit einer X0X Situation an der Diagonale zu schlagen!

Ansonsten hat sich inhaltlich an den Methoden nicht viel geändert, sie sind nur über drei Klassen verteilt.


class SpielStrategie
  def next(spielfeld, aktueller_spieler)
    zufalls_zug(spielfeld, aktueller_spieler)
  end

  private

  def zufalls_zug(spielfeld, aktueller_spieler)
    ...
  end
end

class LeichteSpielStrategie < SpielStrategie
  def next(spielfeld, aktueller_spieler)
    naiver_zug(spielfeld, aktueller_spieler)
  end

  def naiver_zug(spielfeld, aktueller_spieler)
    ...
  end
end

class SchwereSpielStrategie < LeichteSpielStrategie
  def next(spielfeld, aktueller_spieler)
    intelligenter_zug(spielfeld, aktueller_spieler)
  end

  def intelligenter_zug(spielfeld, aktueller_spieler)
    ...
  end
end

Alle drei Klassen haben eine Methode gemeinsam, next(spielfeld, aktueller_spieler). Diese Methode wird von außen aufgerufen, wenn man einen neuen Zug entsprechend der jeweiligen Strategie benötigt. Jede der Strategien macht aber etwas anderes, um diesen nächsten Zug zu berechnen. Die reine SpielStrategie macht zufällig einen Zug, die LeichteSpielStrategie nimmt das nächste frei Feld für den Zug und die SchwereSpielStrategie berechnet einen möglichst schlauen Zug.

Dir fällt sicher etwas Neues auf. Bei der Definition der Klasse LeichteSpielStrategie schreiben wir nach dem Klassennamen eine spitze öffnende Klammer (ein Kleiner-als Zeichen) und danach den Namen einer anderen Klasse SpielStrategie.


class LeichteSpielStrategie < SpielStrategie
  ...
end

Das Kleiner-als Zeichen bedeutet Vererbung. Die Klasse LeichteSpielStrategie erbt alles was die Klasse SpielStrategie zu vererben hat und zwar sind das alle öffentlichen Methoden und Variablen. Man sagt in dieser Vererbungsbeziehung zu der erbenden Klasse Subklasse (sub im Sinne von untergeordnet) und zu der Klasse, die etwas zum Vererben anbietet Superklasse (super im Sinne von übergeordnet).

Und die Klasse SchwereSpielStrategie erbt weiter von LeichteSpielStrategie. Somit bekommt SchwereSpielStrategie auf alle öffentlichen Methoden und Variablen ihrer beiden Superklassen Zugriff.

Vererbung ist ein Mittel um doppelten Code zu vermeiden. Die SchwereSpielStrategie kann die Methode zufalls_zug verwenden, obwohl diese gar nicht innerhalb der Klasse definiert ist. Die Methode stammt aus der Superklasse SpielStrategie von der die SchwereSpielStrategie sie indirekt über die Klasse LeichteSpielStrategie geerbt hat. Klingt kompliziert? Lies dir den Artikel über Vererbung bei Wikipedia durch und nimm auch ein gutes Ruby-Buch zur Hand. Zur Zeit ist eines der besten Bücher The Ruby Way, zwar in Englisch, aber du findest sicher auch ein gutes auf Deutsch.

Fertig!

Jetzt packst du alle Klassen hintereinander weg in eine Datei mit Namen tictactoe.rb und legst noch eine weitere Datei mit Namen lektion_17.rb mit folgendem Inhalt an:


require "tictactoe" 

t = TicTacToe.new
t.strategie = SchwereSpielStrategie.new
t.feldnummerierung_aus
gewinner = t.play(STDOUT, STDIN)
#gewinner = t.play_2_spieler(STDOUT, STDIN)

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

Und schon kannst du Tic-Tac-Toe wie üblich spielen!


[08.02.2008, 23:15]:> ruby lektion_17.rb
/-----------\
|   |   |   |
|---|---|---|
|   |   |   |
|---|---|---|
|   |   |   |
\-----------/
Was spielst du, X oder O? x
Los geht's! Du spielst X, und ich O! Du faengst an.
X ist am Zug: 1
/-----------\
| X |   |   |
|---|---|---|
|   |   |   |
|---|---|---|
|   |   |   |
\-----------/

Jetzt bin ich dran! Ich setze auf das Feld 5.
/-----------\
| X |   |   |
|---|---|---|
|   | O |   |
|---|---|---|
|   |   |   |
\-----------/
X ist am Zug: 
...

Alle vergangenen Lektionen rund um das Spiel Tic-Tac-Toe hier im Überblick. Den vollständigen Sourcecode findest du im Download.