Lektion 7 unter Linux

Erstellt von Frithjof Sat, 12 Oct 2013 10:14:00 GMT

Die Lektion 7 funktioniert jetzt auch unter Linux. Mit den Tasten ‘l’ (klein L) und ‘r’ (klein R) kann man das Fahrrad nach links bzw. rechts fahren lassen. Die Taste ‘x’ beendet die Fahrt.

Aktueller Quellcode zum Download auf GitHub.

Lektion 23 - Oberflächenbewegung

Erstellt von Frithjof Tue, 30 Sep 2008 21:58:00 GMT

Lektion 23 – Oberflächenbewegung

In dieser Lektion erweckst du den LKW zum Leben. Er wird deiner Tastatur und deiner Maus gehorchen und sich auf der Oberfläche von FXRuby vorantasten. Schritt für Schritt wirst du in den kommenden Lektionen dann seine Bewegungen verbessern, sodass es am Ende hoffentlich nicht übertrieben sein wird zu sagen, er fährt!

Bevor wir neue Dinge in das Programm einbauen, strukturieren wir es etwas um. Denn vielleicht möchtest du selbst ein anderes Fahrzeug oder ganz anderes Objekt zeichnen. Dafür ist es besser, das Zeichnen selbst aus der Klasse RubykidsMainWindow auszulagern.

Das Zeichnen des LKW übernimmt die neue Klasse LKW.


class LKW
  def initialize(col_background = FXColor::White)
    @col_background = col_background
    @col_dach       = FXColor::Black
    @col_karosse    = FXColor::Blue
    @col_fenster    = FXColor::DarkBlue
    @col_reifen     = FXColor::Black
    @col_felgen     = FXColor::White
    @col_ruecklicht = FXColor::Red
    @col_blinker    = FXColor::DarkOrange
  end

  def draw(dc, pos = FXPoint.new(0,0))
    return if dc.nil?
    # Einen LKW malen
    # Karosserie
    dc.foreground = @col_karosse
    dc.fillRectangle(pos.x, pos.y,    30, 10) # Oberes Teil
    dc.fillRectangle(pos.x, pos.y+10, 40, 10) # Unteres Teil

    # Dach
    dc.foreground = @col_dach
    dc.drawLine(pos.x, pos.y, pos.x+30, pos.y)

    # Seitenfenster
    dc.foreground = @col_fenster
    dc.fillRectangle(pos.x+2,  pos.y+2, 15, 6) # Hinten
    dc.fillRectangle(pos.x+18, pos.y+2, 10, 6) # Vorne

    # Mit Hintergrundfarbe einen Bereich für die 
    # Räder aus der Karosse schneiden
    dc.foreground = @col_background
    dc.fillCircle(pos.x+8,  pos.y+20, 5) # Hinten
    dc.fillCircle(pos.x+32, pos.y+20, 5) # Vorne

    # Reifen anbringen
    dc.foreground = @col_reifen
    dc.fillCircle(pos.x+8,  pos.y+20, 4) # Hinten
    dc.fillCircle(pos.x+32, pos.y+20, 4) # Vorne

    # Felgen drüber malen
    dc.foreground = @col_felgen
    dc.fillCircle(pos.x+8,  pos.y+20, 2) # Hinten
    dc.fillCircle(pos.x+32, pos.y+20, 2) # Vorne

    # Rücklicht
    dc.foreground = @col_ruecklicht
    dc.fillRectangle(pos.x-1, pos.y+10, 2, 6)

    # Blinklicht vorne
    dc.foreground = @col_blinker
    dc.fillRectangle(pos.x+37, pos.y+12, 3, 2)
  end
end

Die Klasse hat neben dem Constructor initialize, der von außen nur die Hintergrundfarbe mitgeteilt bekommt und die restlichen Fahrzeugfarben festlegt noch die Methode draw. Sie wird mit dem Device Context dc und der Position aufgerufen an die in den dc gezeichnet werden soll.

Die absoluten Werte für die Positionen der geometrischen Objekte (Rechtecke, Kreise, Linie) sind nun relativ zu der von außen gewünschten Position angepasst.

Damit muss das RubykidsMainWindow auch etwas angepasst werden.


class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 400, :height => 300)
    #...

    # Variablen für die Position des LKW
    @current_pos    = FXPoint.new(30, 20)
    @move_distance  = FXPoint.new(18, 18)
    @lkw = LKW.new

    #...
  end

  def onLeinwandRepaint(sender, sel, event)
    FXDCWindow.new(@leinwand, event) do |dc|
      dc.foreground = FXRGB(255, 255, 255)
      dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

      @lkw.draw(dc, @current_pos)
    end
  end

end

Es kommen ein paar neue Instanzvariablen hinzu, current_pos merkt sich die Stelle, an der der LKW gerade ist (d.h. seine linke obere Ecke), move_distance legt fest, in welchen Schritten sich der LKW später in X-Richtung bzw. in Y-Richtung bewegen soll, und natürlich lkw selbst das Objekt, dass den LKW darstellt.

Für Punkte gibt es in FXRuby eine einfache Klasse FXPoint. Dem Konstruktor gibt man zwei Werte mit, den X-Wert und den Y-Wert. Auf beide Werte kann man anschließend mit den Methoden x bzw. y zugreifen.

Das war es auch schon an Vorarbeit. Jetzt bewegen wir ihn.

Maus- und Tastaturinteraktionen mit FXRuby

Der LKW soll
  • bei Klick mit der linken Maustaste vorwärts (also nach rechts),
  • bei Klick mit der rechten Maustaste rückwärts (also nach links),
  • beim Drehen am Mausrad vor- bzw. rückwärts fahren.

Wird eine Maustaste betätigt, wird eine Nachricht von der FXRuby Applikation ausgelöst. Diese Nachricht können wir abfangen und in einer Methode verarbeiten.

Das Abfangen der Nachricht geht wie in der letzten Lektion gesehen mit einem connect:


class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 400, :height => 300)
    #...
    @leinwand.connect(SEL_LEFTBUTTONPRESS,  method(:onMouseDown))
    @leinwand.connect(SEL_RIGHTBUTTONPRESS, method(:onMouseDown))
    @leinwand.connect(SEL_MOUSEWHEEL,       method(:onMouseWheel))
    #...
  end
end

Damit werden die beiden Nachrichten Linksklick und Rechtsklick mit einer gemeinsamen Methode onMouseDown verbunden, während wir die Nachricht über das Betätigen des Mausrades an eine separate Methode onMouseWheel knüpfen.

Die beiden Methoden definieren wir als Methoden der Klasse RubykidsMainWindow wie folgt:


def onMouseDown(sender, sel, event)
  if event.click_button == 1
    # Linke Maustaste => vorwärts
    self.move_forward
  elsif event.click_button == 3
    # Rechte Maustaste => rückwärts
    self.move_backward
  end
end  

def onMouseWheel(sender, sel, event)
  if event.code > 0
    # Vorwärts
    self.move_forward
  elsif event.code < 0
    # Rückwärts
    self.move_backward
  end
end

Wie wir genau die Vorwärts und Rückwärtsbewegung durchführen sehen wir später. Schauen wir uns zunächst an, wie wir dasselbe und noch zwei weitere Bewegungsrichtungen mit der Tastatur verknüpfen können.

Zuerst wieder die Nachricht für einen Tastendruck auf die Leinwand mit einer Methode onKeyPressed verknüpfen.


class RubykidsMainWindow < FXMainWindow
  KEY_ARROW_LEFT  = 65361
  KEY_ARROW_UP    = 65362
  KEY_ARROW_RIGHT = 65363
  KEY_ARROW_DOWN  = 65364

  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 400, :height => 300)
    #...
    @leinwand.connect(SEL_KEYPRESS, method(:onKeyPressed))
    #...
  end
end

Gleichzeitig führen wir ein paar Klassenkonstanten in der Klasse RubykidsMainWindow ein, die den Tastencode für die vier Cursortasten tragen. Diese Konstanten verwenden wir dann in der Methode onKeyPressed zum Erkennen, welche Taste denn genau gedrückt wurde.


def onKeyPressed(sender, sel, event)
  if event.code == KEY_ARROW_LEFT
    # Rückwärts
    self.move_backward
  elsif event.code == KEY_ARROW_RIGHT
    # Vorwärts
    self.move_forward
  elsif event.code == KEY_ARROW_UP
    # Nach oben
    self.move_up
  elsif event.code == KEY_ARROW_DOWN
    # Nach unten
    self.move_down
  end
end

Fertig! Jetzt müssen wir uns wie bereits erwähnt Gedanken machen, wie genau die Bewegungen auf der Leinwandoberfläche vor sich gehen sollen. Vor allem müssen wir klären, was passieren soll, wenn der LKW einen der vier Ränder des Leinwandbereiches erreicht oder sogar darüber hinaus fährt. Soll er in den unsichtbaren Bereich verschwinden? Oder sollen wir es verhindern, dass er dort weiterfährt?

Wir wollen es so gestalten, dass der LKW beliebig fahren kann. Sobald er aber auf einer Seite über den Rand herausfährt, erscheint er gleichzeitig wieder auf der gegenüberliegenden Seite. Das ist so als wären die gegenüberliegenden Ränder hinter dem Fenster herum miteinander verbunden. Die Leinwand wird somit zu einem Ausschnitt auf einem Torus auf dem unser LKW nicht verloren gehen kann.

Um das zu erreichen, sorgen wir zunächst dafür, dass sich die aktuelle Position des LKW immer innerhalb der Leinwand befindet. Sobald die X- oder Y-Koordinate kleiner Null oder größer als die Höhe oder Breite der Leinwand geraten, werden wir sie wieder auf einen Wert zwischen Null und der Höhe bzw. Breite der Leinwand bringen. Das geht am einfachsten mit dem Modulo Operator, den du bereits in Lektion 12 – Tic-Tac-Toe, Eingabe von Zügen kennengelernt hast.

Die vier Bewegungsrichtungen können wir somit wie folgt neu bestimmen:


def move_forward
  @current_pos.x = (@current_pos.x + @move_distance.x) % @leinwand.width
  @current_pos.y = (@current_pos.y                   ) % @leinwand.height
  @leinwand.update
end

def move_backward
  @current_pos.x = (@current_pos.x - @move_distance.x) % @leinwand.width
  @current_pos.y = (@current_pos.y                   ) % @leinwand.height
  @leinwand.update
end

def move_up
  @current_pos.x = (@current_pos.x                   ) % @leinwand.width
  @current_pos.y = (@current_pos.y - @move_distance.y) % @leinwand.height
  @leinwand.update
end

def move_down
  @current_pos.x = (@current_pos.x                   ) % @leinwand.width
  @current_pos.y = (@current_pos.y + @move_distance.y) % @leinwand.height
  @leinwand.update
end

Bei der Vorwärtsbewegung addieren wir zu der X-Richtung einen Distanzschritt hinzu, die Y-Richtung bleibt unverändert. Bei einer Rückwärtsbewegung ziehen wir den gleichen Distanzschritt von der X-Richtung ab, wobei die Y-Richtung erneut unverändert bleibt.

Für die Auf- und Abbewegung machen wir das gleiche, nun allerdings mit der Y-Richtung und die X-Richtung bleibt unverändert.

Am Ende der Addition oder Subtraktion des Distanzschrittes wird das Ergebnis der X- und Y-Richtung nur noch mit dem Modulooperator auf die Breite bzw. Höhe der Leinwand gestutzt. Somit erhalten wir immer eine neue Position, bei der die X-Koordinate größer oder gleich Null aber immer kleiner als die Leinwandbreite ist und die Y-Koordinate ebenfalls größer oder gleich Null aber immer kleiner als die Höhe der Leinwand ist.

Allerdings ist die Bewegung über die Ränder hinweg noch etwas unruhig. Der entscheidende Punkt des LKW ist die obere linke Ecke. Erst wenn sie am Rand aus der Leinwand wandern will, erscheint der LKW schlagartig auf der gegenüberliegenden Seite.

Bei der Illusion eines richtigen Torus würde man erwarten, dass der LKW auf der gegenüberliegenden Seite sofort beginnt zu erscheinen, in dem Maße wie er am Rand verschwindet. Die Herausforderung dabei ist, dass wir feststellen müssten, wieviel vom LKW schon über den Rand hinausragt, um diesen Teil dann auf der gegenüberliegenden Seite zu zeichnen.

Aber es geht einfacher. Schauen wir uns folgende Skizze an

Die Illusion eines Torus können wir am einfachsten erreichen, wenn wir die Leinwand selbst nochmals an ihren Rändern anfügen. Somit erhalten wir insgesamt acht zusätzliche Flächen um die zentrale Leinwand herum. Die obige Skizze verrät schon, was wir dann nur noch machen müssen: wir müssen den LKW nicht nur an der aktuellen Position zeichnen, sondern ihn auch noch um die Höhe oder Breite der Leinwand oder beides zugleich verschoben zeichnen. Der LKW wird somit insgesamt vier mal gezeichnet.

Das erreichen wir schnell durch eine kleine Anpassung der Methode onLeinwandRepaint:


def onLeinwandRepaint(sender, sel, event)
  FXDCWindow.new(@leinwand, event) do |dc|
    dc.foreground = @col_background
    dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

    pos = FXPoint.new(@current_pos.x, @current_pos.y)
    [
      [0,               0],                # Aktuelle Position des LKW
      [@leinwand.width, 0],                # Verschoben nach links
      [0,               @leinwand.height], # Verschoben nach oben
      [@leinwand.width, @leinwand.height]  # Verschoben nach links oben
    ].each do |off|
      pos.x = @current_pos.x - off[0]
      pos.y = @current_pos.y - off[1]

      @lkw.draw(dc, pos)
    end
  end
end

In einer Liste legen wir zunächst die vier Offsets, d.h. die Entfernungen von der aktuellen Position, fest und subtrahieren diese Offsets anschließend von der aktuellen Position und zeichnen den LKW an die dadurch berechnete neue Position.

Die Übergänge an den Rändern werden damit etwas sanfter und der LKW scheint wirklich aus dem einen Rand heraus direkt auf der gegenüberliegenden Seite wieder in die Leinwand hinein zu fahren.

Peter und Livia

Peter: Viel realistischer wäre es, wenn der LKW am Rand stehenbleiben würde.

Livia: Das klingt zwar leichter, ist aber etwas schwieriger zu implementieren. Denn dazu musst du genau erkennen können, ob der LKW an irgendeiner Stelle auch nur einen Pixel über den Rand hinausragen würde. Na dann, viel Spaß beim implementieren.

Lektion 22 - Oberflächentransport 1

Erstellt von Frithjof Sun, 21 Sep 2008 21:44:00 GMT

Die Hello World Oberfläche der letzten Lektion zeigte dir den allgemeinen Umgang mit FXRuby. Damit kannst du aber auf die Dauer niemanden beeindrucken. Zu einer richtigen Oberfläche gehört mehr. Im Vergleich zur Programmierung an der Konsole, wo jede Ausgabe auf einer Zeile stattfindet und der Rest einfach eine Zeile nach oben rutscht, steht dir hier ein bestimmter Bereich an der Oberfläche für Ein- und Ausgaben zur Verfügung, der eine bestimmte Breite und Höhe hat. Ein Widget irgendwo auf dieser Oberfläche hat selbst wieder eine bestimmte Breite und Höhe.

Diese Lektion will dir ein besseres Gefühl für diese zweidimensionale Anordenbarkeit geben.

Du baust eine Oberfläche, die auf der linken Seite mit einer großen Leinwand und auf der rechten Seite mit einer Leiste für Drucktasten ausgestattet ist und noch ein wenig was drumherum.

Allgemeiner Aufbau

Die beiden wichtigen FXRuby Objekte für die Application und das MainWindow legen wir diesmal auch gleich etwas anders an, als in der letzten Lektion.


# Copyright (C) 2007-2008 www.rubykids.de Frithjof Eckhardt
# Alle Rechte vorbehalten.
# lektion_22.rb

require 'fox16'
require 'fox16/colors'

include Fox

class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

  end

  def create
    super
    show(PLACEMENT_SCREEN)

  end

end

application = FXApp.new
    mainWin = RubykidsMainWindow.new(application)
application.create
application.run

Für die Verwendung von Farben stellt FXRuby eine paar Klassen und vordefinierte Variablen bereit, die wir unserem Programm mit dem require ‘fox16/colors’ bekannt machen.

Mit include Fox machen wir das Modul Fox unserem Programm bekannt. Über Module hatten wir noch nicht genauer gesprochen, stell dir einfach eine Klasse darunter vor. Dieses include Fox gestattet uns nun überall den Prefix Fox:: in unserem Programm wegzulassen. Statt Fox::PLACEMENT_SCREEN brauchen wir nun nur noch PLACEMENT_SCREEN zu schreiben.

Als nächstes definieren wir unser eigenes MainWindow als Klasse RubykidsMainWindow. Diese Klasse erbt von der Klasse FXMainWindow. Damit sie sich auch wie ein ordentliches MainWindow verhält, muss sie im Konstruktor initialize ihre Oberklasse mit dem Befehl super aufrufen. Dabei geben wir in dem Parameter opts auch neuerdings gleich die Größe unseres Hauptfensters mit 800 Pixeln Breite und 600 Pixeln Höhe an.

Eine weitere Methode hat unsere Hauptfenster-Klasse. Die Methode create dient dazu, am Anfang das Fenster aufzubauen und alles, was der User von Beginn an sehen soll darauf zu platzieren. Diese create Methode des Hauptfensters wird später von der gleichnamigen create Methode des Application Objekts application.create aufgerufen.

Das probierst du gleich einmal aus.


[21.09.2008, 21:16]:> ruby lektion_22.rb

Es funktioniert zwar, aber es fehlt noch eine Menge.

Die Layoutmanager

Auch wenn eine Oberfläche im Kopf schon fertig ist, lege trotzdem immer noch eine Skizze dafür an. Ich stelle mir unsere Oberfläche, die wir in dieser Lektion schaffen wollen etwas so vor:

Bei einer Oberfläche gibt es neben den sichtbaren Widgets auch unsichtbare Dinger, die bestimmte Aufgaben erfüllen. Dazu gehören die Layoutmanager. Sie sieht man nicht, sie sorgen aber dafür, dass alle von ihnen betreuten Widgets an der Oberfläche richtig angeordnet werden. Ein Layoutmanager ist somit eine Art Container, in den man die Widgets hineinlegt, um die er sich kümmern soll.

Wir verwenden zwei der Layoutmanager von FXRuby. Der FXHorizontalFrame ordnet alle seine Widgets horizontal, also von links nach rechts an und zwar in der Reihenfolge, wie sie ihm übergeben werden. Der FXVerticalFrame Layoutmanager macht dasselbe, nur eben vertikal, also von oben nach unten.

An der Skizze kannst du vielleicht schon ablesen, wieviele solcher Layoutmanager wir brauchen:

Einen FXHorizontalFrame, der sich um den gesamten Bereich kümmert. Nennen wir ihn hauptFrame.

Dem hauptFrame übergeben wir zwei weitere Layoutmanager: Einen FXVerticalFrame, der sich um die Überschrift Malbereich und den weißen Malbereich, die Leinwand, kümmert. Nennen wir ihn leinwandFrame. Und einen FXVerticalFrame für die Überschrift Menü und die eine Drucktaste zum Beenden, nennen wir ihn menuFrame.

Alle drei Layoutmanager erzeugen wir in der initialize Methode unseres eigenen Hauptfensters:


...
def initialize(app)
  super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

  @hauptFrame = FXHorizontalFrame.new(
    self,
    LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y,
    :padLeft   => 0,
    :padRight  => 0,
    :padTop    => 0,
    :padBottom => 0
  )

  # * * * Linker Leinwandbereich
  @leinwandFrame = FXVerticalFrame.new(
    @hauptFrame,
    LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
    :padLeft   => 20,
    :padRight  => 20,
    :padTop    => 20,
    :padBottom => 20
  )

  # * * * Rechter Bereich für Buttons
  @menuFrame = FXVerticalFrame.new(
    @hauptFrame,
    LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
    :padLeft   => 20,
    :padRight  => 20,
    :padTop    => 20,
    :padBottom => 20
  )
end
...

Schauen wir uns den hauptFrame etwas genauer an. Als ersten Parameter erhält er self, also unser eigenes Hauptfenster der Klasse RubykidsMainWindow. Der nächste Parameter ist ein zusammengesetzter Parameter, der etwas ungewöhnlich aussieht. Er besteht aus drei Unterparametern, die mit einem | Strich getrennt sind.

Diese Schreibweise ist in den Sprachen C und C++ üblich, um in einem Parameter gleich mehrere verschiedene Optionen festlegen zu können. Du erinnerst dich an meine einleitenden Worte zu FXRuby? Hier siehst du ein Beispiel für so eine ekelige Sache, die man von dem weiter unten liegenden C++ Programmcode übernommen hat. Das genau zu erklären, wäre Stoff für eine eigene Lektion. Willst du es genau wissen, dann google nach c++ bit operation.

Wir merken uns nur die Bedeutung der Unterparameter. LAYOUT_SIDE_TOP bedeutet, der Layoutmanager fängt mit seiner Arbeit ganz oben (links) an, erstreckt sich dann ganz nach rechts, LAYOUT_FILL_X (das ist die X-Richtung, Breite des Fensters) und ganz nach unten, LAYOUT_FILL_Y (das ist die Y-Richtung, Höhe des Fensters).

Die anderen vier Parameter, die mit pad im Namen Beginnen, legen den Abstand zum umgebenden Rand fest. Also padLeft gibt an, wie groß der Abstand am linken Rand zum umgebenden Bereich sein soll. Achte auch hier auf die Schreibweise bei der Übergabe der Parameter. Es handelt sich bei diesen vier pad-Parametern jeweils um einen Hash mit einem Element. Das ist ein beliebter Trick, um einem Parameter beim Übergeben in einem Methodenaufruf einen Namen zu geben.

Für die beiden anderen Layoutmanager trifft das gesagte ebenfalls zu. Einziger Unterschied ist hier, dass als erster Parameter nicht self, sondern natürlich der hauptFrame übergeben wird, den der soll sich ja um die beiden kümmern.


@leinwandFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
  ...

@menuFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
  ...

Mehr Widgets, mehr Widgets

Jetzt schauen wir uns den linken und rechten Bereich genauer an. Im linken Leinwandbereich fügst du einen Text (Label), eine horizontale Trennlinie (Separator) und schließlich die Leinwand (Canvas) ein.


# * * * Linker Leinwandbereich
@leinwandFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
  :padLeft   => 20,
  :padRight  => 20,
  :padTop    => 20,
  :padBottom => 20
)

FXLabel.new(
  @leinwandFrame, 
  "Malbereich", 
  :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
)

FXHorizontalSeparator.new(
  @leinwandFrame,
  SEPARATOR_GROOVE|LAYOUT_FILL_X
)

@leinwand = FXCanvas.new(
  @leinwandFrame,
  :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT
)

Das Label und den Separator legen wir blind an, d.h. wir legen für sie keine Instanzvariable an. Die Leinwand speichern wir hingegen in der Instanzvariablen @leinwand. Denn auf die Leinwand brauchen wir auch nach dem Erzeugen noch Zugriff.

Und nun noch den rechten Bereich.


# * * * Rechter Bereich für Buttons
@menuFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
  :padLeft   => 20,
  :padRight  => 20,
  :padTop    => 20,
  :padBottom => 20
)

FXLabel.new(
  @menuFrame, 
  "Menü", 
  :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
)

FXHorizontalSeparator.new(
  @menuFrame,
  SEPARATOR_GROOVE|LAYOUT_FILL_X
)

FXButton.new(
  @menuFrame,
  "&Beenden\tAnwendung beenden",
  nil,
  getApp(),
  FXApp::ID_QUIT,
  :opts => FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT,
  :padLeft   => 10, 
  :padRight  => 10,
  :padTop    => 5,
  :padBottom => 5
)

Text und horizontale wie gehabt. Aber die Drucktaste (FXButton) sieht schon etwas komplizierter aus. Zuerst wieder der Layoutmanager, der sich um den Button kümmern soll, das ist klar.

Dann geht es aber schon los. Der zweite Parameter enthält den Text, der auf der Drucktaste erscheinen soll. Das &amp; bedeutet, dass der folgende Buchstabe unterstrichen dargestellt wird und der Button so bei gedrückter ALT-Taste mit diesem Buchstaben angewählt werden kann. In diesem Fall kannst du den Button nicht nur mit der Maus, sondern auch mit der Tastatur durch Drücken von ALT+B auslösen. Der Text ist aber noch nicht zu Ende. Es folgt abgetrennt mit einem Tabulatorzeichen \t ein weiterer Text, der als Tooltip für den Button verwendet wird (das funktioniert aber leider bei mir und bei dem Scribble Beispiel von FXRuby auch nicht).

Der dritte Parameter ist für ein Icon gedacht, da haben wir keins.

Der vierte Parameter gibt ein Objekt an, das beim Auslösen der Drucktaste irgendwie informiert werden soll. Und der fünfte Parameter gibt an, worüber dieses Empfängerobjekt informiert werden soll. In diesem Fall legen wir mit FXApp::ID_QUIT fest, dass der Empfänger (die Applikation selbst) ein QUIT, also ein Beenden von sich selbst einleiten soll.

Dann schauen wir uns das Ergebnis wieder einmal an.


[21.09.2008, 22:52]:> ruby lektion_22.rb

Bis auf den Leinwandbereich müsste nun auch bei dir alles wie gewünscht angezeigt werden. Aber warum wird der Leinwandbereich nicht gezeichnet? Stattdessen sieht man durch das Fenster hindurch auf die darunterliegenden Fenster. Irgendetwas scheint noch zu fehlen.

Die Leinwand (Canvas) ist ein ganz besonderes Widget. Es hat keine vorgegebene Gestalt. Es liegt bei uns, was dort angezeigt wird. Wir müssen es selbst dorthin malen. Wenn wir es nicht tun, dann bleiben die Pixel dort eben mit den Werten belegt, die sie vor dem Aufruf unserer Anwendung schon hatten.

Wir sollten also nun endlich entwas in die Canvas einzeichnen. Das Zeichnen bringen wir aber in einer separaten Methode unserer Hauptfensterklasse unter. Wir nennen sie onLeinwandRepaint und statten sie mit drei Übergabeparametern aus. Die Methode soll nämlich zu bestimmten Ereignissen aufgerufen werden, eben immer dann, wenn der Leinwandbereich neu gezeichnet werden muss. Die Bedeutung wird gleich noch etwas klarer. Hier zunächst die Malmethode für die Leinwand.


# In die Leinwand etwas einzeichnen
def onLeinwandRepaint(sender, sel, event)
  FXDCWindow.new(@leinwand, event) do |dc|
    dc.foreground = FXRGB(255, 255, 255)
    dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

    # Einen LKW malen
    # Karosserie
    dc.foreground = FXColor::Blue
    dc.fillRectangle(30, 20, 30, 10) # Oberes Teil
    dc.fillRectangle(30, 30, 40, 10) # Unteres Teil

    # Seitenfenster
    dc.foreground = FXColor::DarkBlue
    dc.fillRectangle(32, 22, 15, 6) # Hinten
    dc.fillRectangle(48, 22, 10, 6) # Vorne

    # Mit Hintergrundfarbe einen Bereich für die 
    # Räder aus der Karosse schneiden
    dc.foreground = FXColor::White
    dc.fillCircle(38, 40, 5) # Hinten
    dc.fillCircle(62, 40, 5) # Vorne

    # Reifen anbringen
    dc.foreground = FXColor::Black
    dc.fillCircle(38, 40, 4) # Hinten
    dc.fillCircle(62, 40, 4) # Vorne

    # Felgen drüber malen
    dc.foreground = FXColor::White
    dc.fillCircle(38, 40, 2) # Hinten
    dc.fillCircle(62, 40, 2) # Vorne

    # Rücklicht
    dc.foreground = FXColor::Red
    dc.fillRectangle(29, 30, 2, 6)

    # Blinklicht vorne
    dc.foreground = FXColor::DarkOrange
    dc.fillRectangle(67, 32, 3, 2)
  end
end

Wir malen einen weißen Hintergrund mit einem bunten Lastkraftwagen. Das Malen erfolgt über den sogenannten Device Context (Gerätekontext), daher die Abkürzung dc für die Variable. Dieser Gerätekontext repräsentiert im Prinzip den Monitor, oder genauer gesagt nur den Bereich des Monitors, der von unserer Leinwand ausgefüllt wird. An diesen Gerätekontext können wir nun Zeichenbefehle absetzen, die dann am Monitor ankommen. Wir können umrandete Rechtecke (drawRectangle) oder ausgefüllte Rechtecke (fillRectangle) zeichnen, oder auch Kreise (drawCircle bzw. fillCircle). Natürlich auch Punkte (drawPoint), Linien (drawLine) und Polygone (fillPolygon) und einiges mehr.

Den LKW setzen wir nur mit Rechtecken und Kreisen zusammen. Die Farbe wechseln wir dabei an passender Stelle, indem wir die Vordergrundfarbe des Gerätekontextes ändern (dc.foreground). Für die Farben verwenden wir vordefinierte Werte aus der Klasse FXColor, aber man kann auch mit FXRGB innerhalb des RGB Farbraums beliebig eine eigene Farbe auswählen.

Die Methode zum Bemalen des Leinwandbereiches ist nun fertig, aber es funktioniert immer noch nicht. FXRuby weiß ja auch noch nicht, wann es diese Methode aufrufen soll.

Die Application überwacht die gesamte Anwendung, das weißt du aus der Einleitung in der letzten Lektion. Stellt die Anwendung fest, dass ein bestimmtes Widget neu gezeichnet werden muss, weil es bspw. bis jetzt von einem anderen Fenster verdeckt war, so schickt es an das Widget eine Nachricht. Die Nachricht wird in FXRuby als Selektor bezeichnet. Soll ein Widget sich selbst neu zeichnen, so lautet der Selektor SEL_PAINT. Wir müssen der Leinwand also noch sagen, dass sie immer auf diese Nachrichten achten soll. Dazu verbinden wir an der Leinwand den Selektor SEL_PAINT mit der Methode onLeinwandRepaint mit folgendem Methodenaufruf am Leinwandobjekt:


@leinwand.connect(
  SEL_PAINT, 
  method(:onLeinwandRepaint)
)

Nun ruft die Leinwand immer dann, wenn sie sich selbst neu zeichnen soll, die Methode onLeinwandRepaint(sender, sel, event) mit sich selbst als Sender auf.

Hier nun der komplette Code und das Ergebnis beim Aufruf (wie immer auch im Downloadbereich).


# Copyright (C) 2007-2008 www.rubykids.de Frithjof Eckhardt
# Alle Rechte vorbehalten.
# lektion_22.rb

require 'fox16'
require 'fox16/colors'

include Fox

class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

    @hauptFrame = FXHorizontalFrame.new(
      self,
      LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y,
      :padLeft   => 0,
      :padRight  => 0,
      :padTop    => 0,
      :padBottom => 0
    )

    # * * * Linker Leinwandbereich
    @leinwandFrame = FXVerticalFrame.new(
      @hauptFrame,
      LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
      :padLeft   => 20,
      :padRight  => 20,
      :padTop    => 20,
      :padBottom => 20
    )

    FXLabel.new(
      @leinwandFrame, 
      "Malbereich", 
      :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
    )

    FXHorizontalSeparator.new(
      @leinwandFrame,
      SEPARATOR_GROOVE|LAYOUT_FILL_X
    )

    @leinwand = FXCanvas.new(
      @leinwandFrame,
      :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT
    )

    @leinwand.connect(
      SEL_PAINT, 
      method(:onLeinwandRepaint)
    )

    # * * * Rechter Bereich für Buttons
    @menuFrame = FXVerticalFrame.new(
      @hauptFrame,
      LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
      :padLeft   => 20,
      :padRight  => 20,
      :padTop    => 20,
      :padBottom => 20
    )

    FXLabel.new(
      @menuFrame, 
      "Menü", 
      :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
    )

    FXHorizontalSeparator.new(
      @menuFrame,
      SEPARATOR_GROOVE|LAYOUT_FILL_X
    )

    FXButton.new(
      @menuFrame,
      "&Beenden\tAnwendung beenden",
      nil,
      getApp(),
      FXApp::ID_QUIT,
      :opts => FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT,
      :padLeft   => 10, 
      :padRight  => 10,
      :padTop    => 5,
      :padBottom => 5
    )
  end

  # In die Leinwand etwas einzeichnen
  def onLeinwandRepaint(sender, sel, event)
    FXDCWindow.new(@leinwand, event) do |dc|
      dc.foreground = FXRGB(255, 255, 255)
      dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

      # Einen LKW malen
      # Karosserie
      dc.foreground = FXColor::Blue
      dc.fillRectangle(30, 20, 30, 10) # Oberes Teil
      dc.fillRectangle(30, 30, 40, 10) # Unteres Teil

      # Seitenfenster
      dc.foreground = FXColor::DarkBlue
      dc.fillRectangle(32, 22, 15, 6) # Hinten
      dc.fillRectangle(48, 22, 10, 6) # Vorne

      # Mit Hintergrundfarbe einen Bereich für die 
      # Räder aus der Karosse schneiden
      dc.foreground = FXColor::White
      dc.fillCircle(38, 40, 5) # Hinten
      dc.fillCircle(62, 40, 5) # Vorne

      # Reifen anbringen
      dc.foreground = FXColor::Black
      dc.fillCircle(38, 40, 4) # Hinten
      dc.fillCircle(62, 40, 4) # Vorne

      # Felgen drüber malen
      dc.foreground = FXColor::White
      dc.fillCircle(38, 40, 2) # Hinten
      dc.fillCircle(62, 40, 2) # Vorne

      # Rücklicht
      dc.foreground = FXColor::Red
      dc.fillRectangle(29, 30, 2, 6)

      # Blinklicht vorne
      dc.foreground = FXColor::DarkOrange
      dc.fillRectangle(67, 32, 3, 2)
    end
  end

  # Wird von application.create aufgerufen
  def create
    super
    show(PLACEMENT_SCREEN)
  end

end

application = FXApp.new
    mainWin = RubykidsMainWindow.new(application)
application.create
application.run

Wie die X- und Y-Koordinatenwerte für den LKW gewählt wurden, geht aus der folgenden kleinen Skizze etwas anschaulicher hervor:

Peter und Livia

Peter: Fährt das Ding auch? Livia: Bisher nicht, aber mach’ weiter schön mit, dann fährt es vielleicht.

Peter: Auch rückwärts? Livia: Auch um die Kurve!

Lektion 20 - Dort, wo das Bit haust

Erstellt von Frithjof Thu, 31 Jul 2008 22:09:00 GMT

Zahlen bestehen aus Ziffern. Eine Ziffer ist ein einzelnes Symbol. Eine Zahl wird aus einer oder mehreren Ziffern gebildet. Nimm die 21, sie besteht aus den beiden Ziffern 2 und 1. Die Reihenfolge der Ziffern oder die Stelle, an der Ziffern stehen, ist bei Zahlen wichtig. Denn 21 ist nicht dasselbe wie 12. Daher nennt man unser gewöhnliches Zahlensystem auch ein Stellenwertsystem. Jeder Position in einer Zahl ist ein bestimmter Stellenwert zugeordnet und die Ziffer gibt an, wie oft dieser Stellenwert gezählt wird. Unser Zahlensystem ordnet jeder Stelle von rechts nach links ein bestimmtes Vielfache von Zehn zu (1er, 10er, 100er, 1000er, ...) und es besitzt auch zehn verschiedene Ziffern (Symbole) 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 zur Darstellung von Zahlen. Daher nennt man es auch das Zehnersystem oder (wegen lateinisch decem für zehn) Dezimalsystem.

Ein Bit ist eine Ziffer im Zahlensystem der Computerwelt, dem Dualsystem (oder Binärsystem). Hier gibt es nur zwei verschiedene Ziffern, 0 und 1. Jeder Position in einer Bitzahl ordnet man ein Vielfaches von zwei zu (1er, 2er, 4er, 8er, 16er, 32er, 64er, ...). Es scheint aber offensichtlich für einen Computer keine Einschränkung zu sein, dass er nur mit zwei verschiedenen Ziffern umgehen kann.

In dieser Lektion schauen wir uns an, wo die Bits im Computer wohnen, wie wir sie lesen und schreiben können und wie daraus Buchstaben und Zahlen werden, die du am Monitor siehst.

Dateien

Die Daten deines Computers werden auf der Festplatte dauerhaft gespeichert. So bleiben sie auch nach Ausschalten des Computers erhalten. Von der Eingabe bis zur Festplatte müssen sie dabei einen weiten Weg zurücklegen. Umgekehrt natürlich auch. Dabei passiert ziemlich viel, von dem du an der Oberfläche aber glücklicherweise nur wenig mitbekommst.

Zusammengehörige Daten werden in einer Datei zusammengehalten.

Zusammengehörige Dateien wiederum sind in einem “Verzeichnis” gruppiert. Und mehrere Verzeichnisse können wieder in einem Verzeichnis gruppiert werden. So baut sich eine Hierarchie von Verzeichnissen und Dateien auf, das sogenannte Dateisystem. Ganz oben wird das Dateisystem vom Laufwerk (in Windows) zusammengehalten.

Dateien lassen sich direkt auf der Festplatte mit Ruby anlegen. Du probierst das gleich mit einer einfachen Textdatei aus:

Die Datei soll “convolvulus_arvensis.txt” heißen und folgenden Inhalt haben:


Convolvulus arvensis L.
Familie: Convolvulaceae
Ordnung: Solanales
Deutscher Name: Acker-Winde
Blütenfarbe: weiß bis rosa oder gestreift
Blüten: blattachselständig, lang gestielt, Krone weit trichterförmig, 2-3 cm lang
Blätter: pfeil-/spießförmig, 3-4 cm lang
Stängel: dünn, kriechend, windend
Wurzel: bis 2 m tief
Vorkommen: Äcker, Weinberge, Gärten, Wegränder

Dateien schreiben

Hier ist das Rubyprogramm, das diese Datei anlegt.


# lektion_20.rb
File.open("convolvulus_arvensis.txt", "w") do |datei|
  datei.puts "Convolvulus arvensis L." 
  datei.puts "Familie: Convolvulaceae" 
  datei.puts "Ordnung: Solanales" 
  datei.puts "Deutscher Name: Acker-Winde" 
  datei.puts "Blütenfarbe: weiß bis rosa oder gestreift" 
  datei.puts "Blüten: blattachselständig, lang gestielt, Krone weit trichterförmig, 2-3 cm lang" 
  datei.puts "Blätter: pfeil-/spießförmig, 3-4 cm lang" 
  datei.puts "Stängel: dünn, kriechend, windend" 
  datei.puts "Wurzel: bis 2 m tief" 
  datei.puts "Vorkommen: Äcker, Weinberge, Gärten, Wegränder" 
end

Mit File.open wird eine Datei geöffnet. Der erste Parameter gibt den Namen der zu öffnenden Datei an, der zweite Parameter (das “w”) legt fest, wie die Datei geöffnet wird, also in welchem Zustand (Modus) das geschehen soll. Das “w” steht für “write”, die Datei soll also für den Schreibzugriff geöffnet werden.

Die open Methode ist ein Iterator, der die geöffnete Datei an den Codeblock in der Variablen datei übergibt. Am Ende des Blocks kümmert sich der Iterator darum, die Datei wieder zu schließen.

Die Datei legt das Rubyprogramm im selben Verzeichnis an, von dem aus es gestartet wurde. Das muss nicht unbedingt dasselbe Verzeichnis sein, in dem sich das Rubyprogramm befindet. Die Datei wird hier also relativ zum Ausführungsverzeichnis angelegt.

Möchtest du die Datei zuverlässig in einem bestimmten Verzeichnis anlegen, dann teile das Ruby bei File.open genau mit. Der Methode open gibst du dazu nicht nur den Dateinamen, sondern den absoluten Pfad beginnend beim Laufwerk und über alle Verzeichnisse hinweg bis zur Datei an.


# lektion_20.rb
File.open("c:/herbarium/convolvulus_arvensis.txt", "w") do |datei|
  datei.puts "Convolvulus arvensis L." 
  ...
end

Als Trennzeichen beim Pfad kannst du sowohl den normalen Schrägstrich (slash) / verwenden, als auch den nach links gerichteten (back slash). Nimmst du den Backslash, musst du ihn aber zweimal hinschreiben—du erinnerst dich an unsere Häuserreihe?

Dann überprüfe noch schnell, ob die Datei wirklich im Dateisystem dort angekommen ist, wo du sie erwartest. Als nächstes möchten wir die Datei nämlich lesen.

Dateien lesen

Wenn Ruby eine Datei schreiben soll und sie existiert nicht, dann wird zunächst die Datei angelegt und dann der gewünschte Inhalt hineingeschrieben.

Beim Lesen einer Datei ist es etwas unangenehmer, wenn sie nicht existiert. Versuchen wir die zuvor angelegte Datei wieder in das Rubyprogramm einzulesen und auf der Kommandozeile auszugeben.


File.open("c:/herbarium/convolvulus_arvensis.txt", "r") do |datei|
  puts datei.read
end
Der Aufruf der open Methode unterscheidet sich kaum, nur der Modus ist auf “r” (read, d.h. lesen) eingestellt. Das Dateiobjekt in der Variablen datei, das der Codeblock vom Iterator erhält, bietet verschiedene Methoden zum lesen der Datei an. Mit read wird die gesamte Datei eingelesen. Und da wir nur mit Textdateien hier zu tun haben, wird der Inhalt natürlich auch als Text, d.h. als String, zurückgeliefert, wovon du dich mit einem einfachen puts datei.read.class selbst überzeugen kannst.

Das vollständige Lesen einer gesamten Datei ist nicht sinnvoll, wenn sie sehr groß ist. Dann könnte man zum Beispiel zeilenweise lesen mit der Methode each, die eine Zeile nach der anderen liest. So muss nicht die gesamte Datei in den Arbeitsspeicher geladen werden.


File.open("c:/herbarium/convolvulus_arvensis.txt", "r") do |datei|
  datei.each do |zeile|
    puts zeile
  end
end

Festplatte voller Buchstaben?

Alte Computer haben alte Festplatten. Findest du eine alte Festplatte, lass dir nicht die Gelegenheit entgehen, sie aufzuschrauben, um die feine Mechanik und glänzenden Magnetplatten zu bestaunen. Dort wirst du aber keine Buchstaben sehen. Du wirst wohl überhaupt nichts sehen, was irgendwie nach Daten oder Dateien oder Zeichen aussieht.

Ein Computer arbeitet in Wirklichkeit nicht mit Buchstaben oder Texten, sondern nur mit natürlichen Zahlen aus dem Dualsystem—also nur mit 0 und 1 als Ziffern. Egal was du an der Oberfläche eingibst (Texte, Bilder, natürliche Zahlen, Dezimalzahlen, ...), schließlich muss alles in diese Binärzahlen umgewandelt werden und hoffentlich, bevor der Strom ausfällt, werden die Binärzahlen dann in die Festplatte eingekerbt. Ein magnetischer Lese- und Schreibkopf der über die Scheiben der Festplatte saust, kann die Teilchen auf der Scheibe magnetisch in zwei Richtungen ausrichten. Eine der beiden Richtungen wird als die 0 interpretiert, die andere magnetische Teilchenausrichtung als die 1. Ohne Magnetfeld in der Nähe verbleiben die Teilchen auf der Scheibe so wie sie zuvor ausgerichtet wurden.

Bleibt uns nur noch die Frage zu klären, wie aus den Binärzahlen dann die Buchstaben werden und umgekehrt? Dafür gibt es in jedem Betriebssystem eine Tabelle, in der jedem an der Oberfläche darstellbarem Zeichen ein Zahlenwert zugeordnet wird. Diese Tabellen nennt man auch Codepage oder Zeichensatztabelle.

Je nach Größe dieser Tabellen gibt es unterschiedlichste Bezeichnungen. Die wohl umfangreichste ist der Unicode, die bekannteste dürfte aber immer noch der ASCII Zeichensatz sein.

Zum Beispiel ist dem Buchstaben A in der ASCII Tabelle der Zahlenwert 65 im Dezimalsystem (Zehnersystem) zugeordnet. Die 65 wiederum lautet im Dualsystem 1000001 (das kannst du übrigens leicht mit dem Taschenrechner von Windows umrechnen, schalte ihn dazu über “Ansicht” auf “Wissenschaftlich” um). Also für jedes zu speichernde A in einer Textdatei wird auf der Festplatte die binäre Zahlenfolge 1000001 durch den Magnetisierungskopf eingekerbt!

Folgendes kleine Rubyprogramm gibt uns den Inhalt der obigen Textdatei in den verschiedenen Codierungen aus. Zuerst das Zeichen selbst in lesbarer Form, dann den zugehörigen Zahlenwert im Dezimalsystem, im Hexadezimalsystem, im Oktalsystem und letztlich im Dualsystem.


File.open("c:/herbarium/convolvulus_arvensis.txt", "rb") do |file|
  puts "\tDez\tHex\tOkt\tBin" 
  puts "\t---\t---\t---\t---" 
  while(b = file.getc)
    c = b.chr
    puts "#{Regexp.escape(c)},\t#{b},\t0x#{b.to_s(16).upcase},\to#{b.to_s(8).upcase},\t#{b.to_s(2)}" 
  end
end

[31.07.2008, 23:51]:> ruby lektion_20.rb
        Dez     Hex     Okt     Bin
        ---     ---     ---     ---
C,      67,     0x43,   o103,   1000011
o,      111,    0x6F,   o157,   1101111
n,      110,    0x6E,   o156,   1101110
v,      118,    0x76,   o166,   1110110
o,      111,    0x6F,   o157,   1101111
l,      108,    0x6C,   o154,   1101100
v,      118,    0x76,   o166,   1110110
u,      117,    0x75,   o165,   1110101
l,      108,    0x6C,   o154,   1101100
u,      117,    0x75,   o165,   1110101
s,      115,    0x73,   o163,   1110011
\ ,     32,     0x20,   o40,    100000
a,      97,     0x61,   o141,   1100001
r,      114,    0x72,   o162,   1110010
v,      118,    0x76,   o166,   1110110
e,      101,    0x65,   o145,   1100101
n,      110,    0x6E,   o156,   1101110
s,      115,    0x73,   o163,   1110011
i,      105,    0x69,   o151,   1101001
s,      115,    0x73,   o163,   1110011
\ ,     32,     0x20,   o40,    100000
L,      76,     0x4C,   o114,   1001100
\.,     46,     0x2E,   o56,    101110
\r,     13,     0xD,    o15,    1101
\n,     10,     0xA,    o12,    1010
F,      70,     0x46,   o106,   1000110
a,      97,     0x61,   o141,   1100001
m,      109,    0x6D,   o155,   1101101
i,      105,    0x69,   o151,   1101001
l,      108,    0x6C,   o154,   1101100
i,      105,    0x69,   o151,   1101001
...

Weiterführende Literatur

Der Umgang mit Dateien und den verschiedenen Zeichencodierungen ist ziemlich komplex. Daher möchte ich dir ein paar gute Bücher empfehlen, die dir weiterhelfen. Was wir in diesem Artikel besprochen haben, ist nicht mehr als ein kleiner Nunatak.

  • Ruby Cookbook, Lucas Carlson, Leonard Richardson, O’Reilly. Hier gibt es ein umfangreiches Kapitel mit ausführlichen Rezepten rund um Dateien (Anlegen, Lesen, Verzeichnisse anlegen, Binärdateien, ...).
  • The Ruby Way: Solutions and Techniques in Ruby Programming, Hal Fulton, Addison-Wesley

Mit Dateien werden wir noch viel zu tun bekommen in den nächsten Lektionen. Nicht nur mit Textdateien, sondern insbesondere mit Binärdateien (Bilder).

Lektion 19 - Containers, Blocks und Iterators 2

Erstellt von Frithjof Sat, 01 Mar 2008 07:29:00 GMT

Eine der Stärken von Ruby sind die leicht zu benutzenden Datenstrukturen, die als Behälter für alle möglichen Daten dienen. Du hast von diesen Container genannten Strukturen bereits Listen und Hashes kennengelernt.

In dieser Lektion lernst du die beiden anderen Talente von Ruby kennen: Blocks (engl. für Blöcke) und Iterators (engl. für Iteratoren, aus dem lateinischen iter, das Gehen, der Weg oder der Marsch).

Dann, mit der nächsten Lektion 20, geht dieser Teil von rubykids.de schließlich zu Ende und es beginnt mit Lektion 21 ein neuer. Mal sehen, was wir da spannendes machen werden.

Blocks und Iterators

Die Geschichte von Blöcken und Iteratoren ist schnell erzählt. Der Iterator (ein alter Römer) greift sich während seines beschwerlichen Marsches entlang eines bis zum Rand mit Eisen gefüllten Containers nacheinander einen Klumpen des harten Elements, erhitzt ihn, legt ihn auf einen Block und haut solange ordentlich drauf, bis ein geschmeidiger Stahl den Amboss wieder verlässt.

Einige hundert Jahre später können wir das mit Ruby etwa wie in folgendem Beispiel formulieren:


eisenklumpen_container = ["klumpen1", "klumpen2", "klumpen3", "klumpen4"]

eisenklumpen_container.each do |klumpen|
  puts "Erhitze den #{klumpen}..." 
  puts "Schlage den #{klumpen} auf dem Amboss..." 
  puts "... zu geschmeidigem Stahl." 
  puts
end

Die Liste eisenklumpen_container ist ein Objekt der Container-Klasse Array. Der alte römische Iterator ist hier each, eine Methode der Klasse Array. Der Iterator each ruft für jedes Element klumpen im eisenklumpen_container den vier-zeiligen Codeblock auf, der sich zwischen do und end befindet. Im Verlauf macht der Iterator das Element, das er gerade verarbeitet dem Codeblock über die Variable klumpen bekannt.

Bisher hatten wir für die aufeinanderfolgende Bearbeitung von Elementen aus einem Container mit der for Schleife hantiert. Das obige Beispiel würde sich dann so darstellen:


eisenklumpen_container = ["klumpen1", "klumpen2", "klumpen3", "klumpen4"]

for klumpen in eisenklumpen_container do 
  puts "Erhitze den #{klumpen}..." 
  puts "Schlage den #{klumpen} auf dem Amboss..." 
  puts "... zu geschmeidigem Stahl." 
  puts
end

Hier gibt es nur den Container und den Codeblock, aber keinen Iterator. Zumindest sieht man ihn nicht. Der Rubyinterpreter ersetzt aber hinter den Kulissen die for-Schleife durch einen each-Iterator. Beides ist somit gleichwertig verwendbar.

Der each-Iterator ist schon ganz prima, nur ist er nicht überall sinnvoll einsetzbar. Stellen wir uns einen Hash vor, den aus der Theorie Lektion 5. Was sollte bei einem Hash ein each Iterator tun? Soll er alle Schlüssel des Hash ablaufen, oder alle Werte? Oder beides zugleich?


deutsch_spanisch = {
  "eins"   => "uno",
  "zwei"   => "dos",
  "drei"   => "tres",
  "vier"   => "cuatro",
  "fuenf"  => "cinco", 
  "sechs"  => "seis",
  "sieben" => "siete",
  "acht"   => "ocho",
  "neun"   => "nueve",
  "zehn"   => "diez",
}

deutsch_spanisch.each_key do |key|
  puts "Iterator bearbeitet gerade den Schluessel: #{key}" 
end

deutsch_spanisch.each_value do |value|
  puts "Iterator bearbeitet gerade den Wert: #{value}" 
end

deutsch_spanisch.each_pair do |key, value|
  puts "Iterator bearbeitet gerade das Paar: #{key}, #{value}" 
end

Ein Hash hat also sogar drei verschiedene Iteratoren! Der Iterator each_key durchläuft alle Schlüssel des Hash nacheinander, während der Iterator each_value sich die Werte vornimmt. Der each_pair Iterator schnappt sich sowohl den Schlüssel, als auch den zugehörigen Wert aus dem Hash und wirft ihn dem Code-Block zur Verarbeitung vor.

Dann wollen wir doch mal sehen, ob das Erfinden eines Iterators auch so einfach ist, wie seine Verwendung.

Einen Iterator bauen

In diesem Abschnitt erstellen wir einen eigenen Iterator. Er soll each_mit_nachfolger heißen und wie sein kleiner Bruder each auch alle Elemente einer Liste abschreiten, dabei aber gleichzeitig immer auch das nachfolgende Element mit anfassen. Gewünscht sei folgende Verwendungsmöglichkeit für den Iterator.


liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

Wir legen dafür eine Klasse MeineListe an und spendieren ihr eine einzige Methode each_mit_nachfolger als Iterator. MeineListe soll sich ansonsten genauso wie ein gewöhnliches Array verhalten, das bedeutet sie erbt von der Klasse Array. Oder wir sagen dazu auch wir leiten die Klasse MeineListe von der Klasse Array ab.


class MeineListe < Array
  def each_mit_nachfolger
  end
end

Bisher also nichts besonderes. Aber die Methode each_mit_nachfolger ist noch lange kein Iterator. Sie macht bisher ja noch überhaupt nichts.

Lassen wir sie zuerst dasselbe machen wie der Iterator each.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
    end
  end
end

Der Aufruf von each innerhalb von each_mit_nachfolger funktioniert deswegen, weil each eine Methode der Klasse Array ist und durch die Vererbung ist diese Methode auch auf die Klasse MeineListe übergegangen.

Das Element element müssen wir nun an den Block weitergeben, der vielleicht beim Aufruf der Methode each_mit_nachfolger mitgegeben wurde. Für dieses Aufrufen eines Code-Blocks und das Weitergeben von Werten an diesen Block hat Ruby ein spezielles Schlüsselwort parat: es heißt yield.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
      yield(element) if block_given?
    end
  end
end

yield steht stellvertretend für den unsichtbaren Code-Block. Und wie bei einem gewöhnlichen Methodenaufruf können wir yield auch als Parameter die Werte mitgeben, die dann im Block zur Verarbeitung verfügbar sein sollen. Falls kein Block beim Aufruf übergeben wird, darf yield nicht aufgerufen werden. Daher der Schutz von yield mit der if-Abfrage. Andernfalls würde der Aufruf des Iterators ohne Code-Block zu einem Fehler führen.


liste.each_mit_nachfolger

C:\entwicklung>ruby lektion_19.rb
lektion_19.rb:56:in `each_mit_nachfolger': no block given (LocalJumpError)
        from lektion_19.rb:52:in `each'
        from lektion_19.rb:52:in `each_mit_nachfolger'
        from lektion_19.rb:68

Die Methode block_given? wird von der Klasse Object bereitgestellt (über das Modul Kernel, Module schauen wir uns später noch an) und ist in jeder anderen Klasse automatisch verfügbar, denn am oberen Ende jeder Vererbungshierarchie steht die Klasse Object. Das trifft auch für unsere selbst definierte Klasse MeineListe zu, wie folgende irb Sitzung schnell beweist:


C:\entwicklung>irb
irb(main):001:0> require 'lektion_19'
=> true
irb(main):002:0> MeineListe.superclass
=> Array
irb(main):003:0> Array.superclass
=> Object
irb(main):004:0>

Nun gut, dann probieren wir unseren selbst gebastelten Iterator einmal aus.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
      yield(element) if block_given?
    end
  end
end

liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

C:\entwicklung>ruby lektion_19.rb
'eins' kommt in der Liste vor ''
'zwei' kommt in der Liste vor ''
'drei' kommt in der Liste vor ''
'vier' kommt in der Liste vor ''

Prima, alle Element der Liste werden abgearbeitet. Aber das konnte der each Iterator auch schon. Wir passen unseren eigenen Iterator jetzt so an, dass er wirklich neben dem aktuellen Element in der Liste den jeweils aktuellen Nachfolger gleich mit bearbeitet.


class MeineListe < Array
  def each_mit_nachfolger
    akt_nachfolger = nil
    akt_element    = nil
    each do |element|
      akt_element    = akt_nachfolger
      akt_nachfolger = element
      if akt_element != nil
        yield(akt_element, akt_nachfolger) if block_given?
      end
    end
  end
end

liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

Und dann sieht die Ausgabe wie erwartet aus:


C:\entwicklung>ruby lektion_19.rb
'eins' kommt in der Liste vor 'zwei'
'zwei' kommt in der Liste vor 'drei'
'drei' kommt in der Liste vor 'vier'

Der Iterator hält rechtzeitig bei der drei mit Nachfolger vier an, weil nach der vier die Liste ja zu Ende ist.

Blöcke als Konservendose

Ein Iterator übergibt alle Elemente, die er abläuft an einen Code-Block. Die Zeilen des Code-Blocks stehen dabei zwischen dem do und dem end. Das muss aber nicht so sein.

Wie der Name Block schon sagt, kann ein Block auch alleine für sich stehen.


element    = nil
nachfolger = nil
ein_block = Proc.new {  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" }
ein_block.call

C:\entwicklung>ruby lektion_19.rb
'' kommt in der Liste vor ''

Man kann Rubycodezeilen in einer Variablen speichern! Alle Codezeilen werden in einem Objekt der Klasse Proc verwaltet. Dieses Objekt ist es dann, was in der Variablen festgehalten wird. Über die Methode call der Klasse Proc lässt sich der Codeblock dann leicht aufrufen. In dem kleinen Beispiel tut sich aber nichts, weil noch niemand die Variablen element und nachfolger sinnvoll belegt hat. Ein Codeblock macht somit nur Sinn, wenn er in einer passenden Umgebung verwendet wird, wie zum Beispiel beim Aufruf eines Iterators:


element    = nil
nachfolger = nil
ein_block = Proc.new {  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" }
liste.each_mit_nachfolger do |element, nachfolger|
  ein_block.call
end

puts element
puts nachfolger
ein_block.call

Code-Blöcke haben eine weitere interessante Eigenschaft, die ebenfalls zu den wichtigen Kernkompetenzen von Ruby gehört. Ein Codeblock merkt sich den Kontext, in dem er erzeugt wurde. Mit Kontext sind hier insbesondere alle Variablen gemeint, die bei der Anlage des Codeblocks erreichbar sind. Dabei merkt sich der Codeblock nicht den aktuellen Wert der Variable, sondern die Variable selbst. Und damit kann der Codeblock jedesmal, wenn er aufgerufen wird den dann zum Aufrufzeitpunkt aktuellen Variablenwert verwenden.

Im letzten Beispiel oben haben wir am Ende des Aufrufs vom Iterator each_mit_nachfolger nochmal die Werte der beiden Variablen element und nachfolger ausgegeben. Sie waren jetzt am Ende des Interatordurchlaufs natürlich mit den beiden letzten Werten belegt, die vom Iterator angefasst wurden, also drei und vier. Rufen wir danach außerhalb des Iterators nochmal den Codeblock auf, so verwendet er ja dieselben Variablen, die bei seiner Erzeugung schon bekannt waren, also element und nachfolger, natürlich aber mit ihrem jeweiligen aktuellen Werten.

Vielleicht hilft noch ein weiteres kleines Beispiel, um dieses auf den ersten Blick merkwürdige Verhalten zu verstehen. Betrachten wir folgenden Code:


sag_hallo = nil

1.times do
  name = "Peter" 
  sag_hallo = Proc.new { puts "Hallo #{name}!" }
  name = "Eulalia" 
end

name = "Livia" 

sag_hallo.call

Wir definieren eine Variable sag_hallo für einen Codeblock. Dann rufen wir den Iterator times der Klasse Fixnum für die Zahl 1 auf. Innerhalb des Iteratoraufrufs definieren wir eine lokale Variable name und setzen sie auf den Wert Peter. Anschließend erst erzeugen wir den Codeblock selbst, auch innerhalb des Iteratoraufrufs times. Danach setzen wir die Variable name auf einen anderen Wert und verlassen den Iterator.

Wieder aus dem Iterator draußen setzen wir die Variable name auf den Wert Livia und rufen anschließend den Codeblock sag_hallo auf. Was wird er ausgeben? Es gibt drei Möglichkeiten:

  1. Hallo Peter!
  2. Hallo Eulalia!
  3. Hallo Livia!

Probieren wir es einfach aus:


C:\entwicklung>ruby lektion_19.rb
Hallo Eulalia!

Die Begründung ist wieder der Kontext bei der Erzeugung des Codeblocks. Zum Zeitpunkt, als der Codeblock sag_hallo angelegt wird, ist eine Variable name bekannt, die aber nur lokal innerhalb des Iteratoraufrufs times sichtbar ist.

Die gleichnamige Variable name außerhalb des Iteratoraufrufs ist eine andere Variable!

Es gibt hier also zwei verschiedene Variablen mit dem gleichen Namen! Zum Zeitpunkt der Erzeugung des Codeblocks aber war nur die erste der beiden bekannt. Daher verwendet der Codeblock bei seinem Aufruf auch den letzten aktuellen Wert dieser Variablen.

Zur Kontrolle dieser Beobachtung definieren wir die Variable name einmal vor dem Iteratoraufruf:


sag_hallo = nil
name      = nil

1.times do
  name = "Peter" 
  sag_hallo = Proc.new {puts "Hallo #{name}!"}
  name = "Eulalia" 
end

name = "Livia" 

sag_hallo.call

Jetzt gibt es im gesamten Programm nur eine einzige Variable name und somit verwendet der Codeblock bei seinem letzten Aufruf auch den dann aktuellen Wert dieser Variable:


C:\entwicklung>ruby lektion_19.rb
Hallo Livia!

Man nennt einen Codeblock in diesem Zusammenhang auch gerne eine Hülle, oder englisch Closure. Er hüllt alles in sich hinein, was zum Zeitpunkt seiner Entstehung in der Umgebung bekannt ist. Er legt seinen einhüllenden Mantel um alle Variablen, die bei seiner Erschaffung sichtbar sind. Alles was erst später zu Tage tritt, kennt er nicht.

Lektion 18 - Gib ihr dein Ruby!

Erstellt von Frithjof Fri, 22 Feb 2008 21:25:00 GMT

Peter hat ein Programm in Ruby für Livia entwickelt und möchte es ihr zum Geburtstag schenken. Livia leidet an Unpünktlichkeit. Also hat er die letzten Tage vor dem großen Fest damit verbracht, ihr eine Zeitansage in Ruby zu schreiben. Den Quellcode teilte er auf zwei Dateien auf. Nun steht er vor der Frage, wie er ihr seine kreativen Zeilen zukommen lässt. Soll er beide Dateien als Anhang an seine Glückwunsch E-Mail kleben? Soll er sie gemeinsam in eine Zipdatei packen? In dieser Lektion lernst du, wie du deine coolen Programme an andere weitergibst. Der Quellcode zu dieser Lektion befindet sich wie immer im Downloadbereich.

Peter entscheidet sich für den Standardweg, um sein Rubyprogramm als Packet für Livia verfügbar zu machen: er erstellt ein GEM (engl. Edelstein) mit Hilfe von RubyGems, dem Packetsystem für Ruby.

Mit RubyGems kann man aber nicht nur ein Packet einer Ruby Anwendung erstellen, sondern diese Packete auch auf dem lokalen Computer verwalten, d.h. neue Gems installieren, bestehende deinstallieren, nach neuen suchen oder ein existierendes Gem auf eine neue Version updaten.

Konkret gesprochen ist ein Gem eine einzige Datei, genauer eine Archivdatei, die eine oder mehrere Dateien in komprimierter Form enthält. Die Erstellung so einer Datei erfordert den RubyGems Packetmanager.

Der Befehl gem auf der Kommandozeile teilt dir leicht mit, ob RubyGems bei dir bereits verfügbar ist.


C:\entwicklung> gem

RubyGems is a sophisticated package manager for Ruby.  This is a
basic help message containing pointers to more information.

  Usage:
    gem -h/--help
    gem -v/--version
    gem command [arguments...] [options...]

  Examples:
    gem install rake
    gem list --local
    gem build package.gemspec
    gem help install

  Further help:
    gem help commands            list all 'gem' commands
    gem help examples            show some examples of usage
    gem help <COMMAND>           show help on COMMAND
                                   (e.g. 'gem help install')
  Further information:
    http://rubygems.rubyforge.org

Der Packetmanager kommt gewöhnlich mit der Installation von Ruby auf dein System. Andernfalls musst du ihn zu Fuß installieren.

Peter hat einiges zu tun. Er teilt sich die Arbeit folgendermaßen ein:

  1. Das Zeitansageprogramm entwickeln
  2. Das Programm für den Ruby GEM Packetmanager vorbereiten. Dazu wird eine bestimmte Verzeichnisstruktur wie folgt angelegt:
    • Unterverzeichnis lib mit der Datei zeitansage.rb
    • Unterverzeichnis bin mit der Datei wie_spaet (das ist eine Rubydatei, aber ohne Endung .rb)
    • README Datei anlegen, die das Programm beschreibt
    • LICENSE Datei anlegen, die die Software Lizenz zum Programm enthält
  3. Das Gem mit Hilfe des RubyGems Packetmanagers aus einer GEM-Spezifikation erzeugen
  4. Testinstallation auf seinem lokalen Computer ausprobieren

Das Zeitansageprogramm

Peters Progrämmchen ist recht einfach. Es besteht aus der Klasse Zeitansage, deren Konstruktor (Methode initialize) eine Liste mit den Anreden und ein Muster für die Ausgabe der Uhrzeit erhält.


class Zeitansage
  def initialize(anreden, zeitmuster)
    @anreden    = anreden
    @zeitmuster = zeitmuster
  end

  def es_ist
    zeit = Time.now.strftime(@zeitmuster)
    "#{anrede} #{zeit}" 
  end

  private

  def anrede
    text = "" 
    unless @anreden.nil?
      idx = zufalls_zahl(@anreden.length)
      text = @anreden[idx]
    end
    text
  end

  # Liefert keine wirkliche Zufallszahl, für diese
  # Zwecke hier ist es aber ausreichend.
  def zufalls_zahl(max)
    (max > 0) ? (Time.now.sec % max) : 0
  end
end

Peter überlegt sich ein paar Anreden, wie etwa Hallo Livia! oder Oh, meine Prinzessin!” und legt sie in einer Liste anreden ab.


anreden = [
  "Hallo Livia!", 
  "Hi Livia!",
  "Liebe Livia!",
  "Oh, meine Prinzessin!",
  "Wozu willst du das schon wieder wissen?",
  "Sollte ich dir lieber eine Armbanduhr schenken?",
  "Ich sag nichts ohne meinen Rubyinterpreter!",
]

Bei der Ausgabe der Zeit ist er weniger kreativ und wählt folgendes Muster:


muster = "Es ist %H:%M Uhr." 

Die Prozentzeichen jeweils gefolgt von einem Buchstaben sind Platzhalter, die später dann mit den entsprechenden Werten der Zeit oder Datum ersetzt werden. So wird %H durch den Wert der Stunden und %M durch den Wert der Minuten ersetzt. Es gibt noch viel mehr solche Platzhalter für Zeit und Datum.

Ein Objekt der Klasse Zeitansage wird mit der Liste der Anreden und dem Zeitausgabemuster zum Leben erweckt und schon ist das Programm wie_spaet für Livias Geburtstag fertig!


require 'zeitansage'

anreden = [
  "Hallo Livia!", 
  "Hi Livia!",
  "Liebe Livia!",
  "Oh, meine Prinzessin!",
  "Wozu willst du das schon wieder wissen?",
  "Sollte ich dir lieber eine Armbanduhr schenken?",
  "Ich sag nichts ohne meinen Rubyinterpreter!",
]

muster = "Es ist %H:%M Uhr." 

z = Zeitansage.new(anreden, muster)
puts z.es_ist

Das Programm für den Ruby GEM Packetmanager vorbereiten

Dafür legt Peter zuerst eine Verzeichnisstruktur an, wie sie der Packetmanager verlangt. Das ausführbare Programm kommt in eine endungslose Datei wie_spaet in das Unterverzeichnis bin, die Klasse Zeitansage in eine separate Datei zeitansage.rb in das Unterverzeichnis lib.

Livia wird sicher in seinem Programmcode herumstöbern. Peter entschließt sich, ihr eine README Datei zu hinterlassen, sodass sie sich leichter in den komplexen Code einarbeiten kann.


= wie_spaet -- Ein Ruby Programm zur Zeitansage

Dieses Softwarepacket enthält 'wie_spaet', ein einfaches 
Rubyprogramm, dass die aktuelle Uhrzeit ausgibt.

wie_spaet hat die folgenden Features:

* Anreden: Die Zeitansage kann mit einer Liste von Anreden initialisiert
  werden. Aus der Liste wird bei jedem Aufruf mehr oder wenig zufällig
  eine Anrede ausgewählt. 

* Zeitausgabe: Das Ausgabeformat der Zeitansage kann ein mit den
  üblichen Platzhaltern für die Zeit- und Datumswerte versehene
  Zeichenkette sein.

== Download

Die aktuelle Version von 'wie_spaet' gibt es unter

* http://www.rubykids.de

== Installation

=== GEM Installation

wie_spaet kann als GEM gedownloaded werden und wird anschließend
mit folgendem Befehl installiert (je nach Version):

   gem install wie_spaet-0.0.1.gem

== Verwendung von wie_spaet

=== Direkte Verwendung 

Ist wie_spaet installiert, kann es einfach auf der 
Kommandozeile aufgerufen werden:

  C:\> wie_spaet

Jedes Softwarepacket, das man an andere weitergeben möchte benötigt eine Lizenz, damit für alle klar ist, ob die Software frei verwendet werden darf, ob sie etwas kostet oder ob man zu jeder Verwendung den Urheber um Erlaubnis fragen oder ihm gar noch was dafür bezahlen muss.

Peter ist da sehr großzügig und möchte sein Programm natürlich als OpenSource Software zur freien Nutzung aller Welt (insbesondere Livia) zur Verfügung stellen. Er kann sich aber nicht recht entschließen, welche OpenSource Lizenz die passende ist und erfindet kurzer Hand seine eigene.


Copyright (C) 2008 Peter @ www.rubykids.de

Peter's Geburtstags-Lizenz
Version 1.0, Februar 2008

Hiermit hat jeder, der eine Kopie dieser Software erhält, die Erlaubnis, 
sie kostenfrei unter den folgenden Bedingungen uneingeschränkt zu verwenden, 
zu kopieren, zu verändern, zu veröffentlichen und weiterzugeben:

* Der obige Copyright Hinweis und diese Erlaubnis muss in allen Kopien 
  und Teilen der Software enthalten sein.
* Der Empfänger der Software ist ein Kind, oder fühlt sich wie ein Kind, 
  oder kann sich zumindest während der Verwendung der Software 
  wie ein Kind benehmen.

Die Verzeichnisstruktur sieht bisher etwa folgendermaßen aus:

GEM mit Packetmanager und Spezifikation erzeugen

Die sogenannte Spezifikation legt genau fest, welche Teile aus der oben angelegten Verzeichnisstruktur mit in das Packet übernommen werden sollen. Die Spezifikation ist gewöhnlicher Rubycode in einer Datei mit der Endung gemspec, Peter nennt sie wie_spaet.gemspec.


spec = Gem::Specification.new

spec.name         = "wie_spaet" 
spec.version      = "0.0.1" 
spec.authors      = ["rubykids"]
spec.summary      = "Sagt dir die Zeit." 
spec.homepage     = "http://www.rubykids.de/" 
spec.description  = "Waehlt zufaellig eine Anrede und sagt dir die Zeit." 
spec.files        = [ "README", "LICENSE", "bin/wie_spaet", "lib/zeitansage.rb"]
spec.executables  = ["wie_spaet"]
spec.rdoc_options = ["--charset", "UTF-8", "--line-numbers", 
                     "--inline-source", "--main", "README", "--title", 
                     "wie_spaet -- Sagt dir die Zeit!", "--all"]
spec.has_rdoc     = true
spec.extra_rdoc_files = ["README", "LICENSE", "bin/wie_spaet"]

Jede GEM-Spezifikation zu einem RubyGems Packet sieht etwa so aus. Zuerst wird ein Objekt der Klasse Gem::Specification in der Variablen spec angelegt (was die Doppelpunkte im Klassennamen bedeuten haben wir noch nicht behandelt, das kommt noch).

Dann werden einige Attribute dieses Objektes gesetzt. Es gibt zwingend notwendige Attribute wie etwa name und version und optionale. Was die Attribute genau bedeuten ist aus ihren Bezeichnungen eigentlich meistens klar. Wir können sie hier nicht im Detail besprechen. Du kannst Genaueres aber gerne in einem Rubybuch oder im Online RubyGems Manual nachlesen.

Die Datei mit der GEM-Spezifikation legt Peter ganz oben in der Verzeichnisstruktur neben LICENSE und README ab und ruft dann in diesem Verzeichnis an der Kommandozeile den Packetmanager für RubyGems auf, um endlich das Packet für den Geburtstag zu schnüren:


C:\entwicklung\lektion_18>gem build wie_spaet.gemspec
  Successfully built RubyGem
  Name: wie_spaet
  Version: 0.0.1
  File: wie_spaet-0.0.1.gem

Es entsteht eine Datei wie_spaet-0.0.1.gem. In dieser Datei schlummert sein gesamtes Rubyprogramm, für Livia.

Sobald Livia das GEM erhält, kann sie es installieren:


C:\livia\geschenke>gem install wie_spaet-0.0.1.gem
Successfully installed wie_spaet, version 0.0.1
Installing ri documentation for wie_spaet-0.0.1...
Installing RDoc documentation for wie_spaet-0.0.1...

und nachschauen, ob es wirklich installiert ist:


C:\livia\geschenke>gem list --local wie_spaet

*** LOCAL GEMS ***

wie_spaet (0.0.1)
    Sagt dir die Zeit.

und es natürlich verwenden:


C:\livia\geschenke>wie_spaet
Oh, meine Prinzessin! Es ist 23:31 Uhr.

Was mit dem GEM nach dem Installieren passiert

Bei der Installation des GEM landet das gesamte Packet im Verzeichnisbaum der Rubyinstallation im Zweig ruby\lib\ruby\gems\1.8. im Unterordner wie_spaet-0.0.1.

Zusätzlich wird für die Datei wie_spaet aus dem bin Verzeichnis eine korrespondierende, an der Kommandozeile ausführbare Datei im bin Verzeichnis der Rubyinstallation abgelegt. Diese Datei wie_spaet.cmd kann dann direkt an der Kommandozeile mit wie_spaet aufgerufen werden.

Die GEM Spezifikationsdatei wie_spaet.gemspec taucht separat im Verzeichnis ruby\lib\ruby\gems\1.8\specifications auf.

Nachdem nun Peter die Installation überprüft hat, ist er mit seinem GEM zufrieden, findet bei seiner Lieblingsfloristin gegenüber noch ein paar hübsche Blumen, kopiert sein Gem auf einen USB-Stick und versteckt ihn zusammen mit einer Grußkarte in dem Strauß. Die Angst, Livia könnte in seinem Geburtstagsgeschenk einen Bug finden, schüttelt er ab als die Haustür hinter ihm ins Schloß fällt. Wird sich Livia freuen? Na bestimmt!

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.

Lektion 16 - Objekte - na endlich!

Erstellt von Frithjof Sat, 17 Nov 2007 00:13:00 GMT

In den vergangenen Lektionen drehte sich alles um das Spiel Tic-Tac-Toe. Wir haben wie verrückt Ruby Code getippt und – so hoffe ich – fleißig Tic-Tac-Toe gespielt, zu zweit am und gegen den Computer. Der ist im Verlauf unserer Tätigkeiten ziemlich stark geworden. Irgendwie macht es dann mit der Zeit keinen Spaß mehr, weil man ja sowieso schon weiß, dass es wieder mal auf ein Unentschieden hinaus läuft.

Hier nochmal alle Lektionen zu dem Spiel im Überblick.

Und jetzt? Wir schauen uns in dieser Lektion etwas an, wovon sicher viele meinen, ich hätte es dir schon eher sagen sollen.

Machen wir uns ein wenig darüber Gedanken, wie wir bisher programmiert haben. Schau dir mal die letzte Datei tictactoe.rb an. Sie besteht aus zahlreichen Methoden (es müssten 16 Stück sein), die immer mit def gefolgt von einem Namen und einer Liste von Übergabeparametern anfangen und mit end abgeschlossen werden. Jede Methode ist eine Art kleines Progrämmchen, das einen kleinen Teil der Arbeit des gesamten Programms übernimmt. Sie wird von irgendwo aufgerufen, erledigt ihre Teilaufgabe und gibt möglicherweise ein Ergebnis an den Aufrufer zurück. Statt Methode sagen viele auch Prozedur dazu. Prozedur leitet sich vom Lateinischen Wort für vorwärts gehen ab.

Du stellst dir eine Methode (oder Prozedur) somit als eine Menge von aufeinander folgenden Programmanweisungen vor, die vom Rubyinterpreter der Reihe nach abgearbeitet werden. Der Rubyinterpreter geht gewissermaßen vorwärts durch die Codezeilen und macht, was sie ihm befehlen.

Wirf nochmal einen Blick in die Datei tictactoe.rb, insbesondere die ersten Zeilen der Methoden:


  def spielfeld(out, zuege)
  def print_zeile(out, zeile, zuege)
  def print_feld(spalte, zeile, zuege)
  def zug_hinzu(wer, spalte, zeile, zuege)
  def nummer_in_spalte_zeile(num)
  def nummer_aus_spalte_zeile(spalte, zeile)
  def the_winner_is(zuege)
  def ist_beendet?(zuege)
  def play_2_spieler(out, ein, zuege)
  def zufalls_zug(zuege, spieler, wer)
  def naiver_zug(zuege, spieler, wer)
  def freie_felder(zuege)
  def reihen_status(zuege, reihe)
  def intelligenter_zug(zuege, spieler, wer)
  def computer_zug(zuege, spieler, wer)
  def play_gegen_computer(out, ein, zuege)

Fällt dir etwas auf? Stichwort: Wiederholungen? Von den 16 Methoden, haben 14 die Variable zuege in ihrer Liste der Übergabeparameter! Nur auf zwei Methoden nummer_in_spalte_zeile und nummer_aus_spalte_zeile trifft das nicht zu.

Die Menge der Methoden lässt sich somit in zwei Gruppen einteilen: Methoden, die von der Liste der im Spiel gemachten zuege abhängen (die 14 Stück) und Methoden, die nicht von zuege abhängen (die 2 übrigen).

Die 14 abhängigen Methoden benötigen Wissen über die gemachten Spielzüge, daher muss man ihnen diese beim Aufruf mitgeben. Würden wir das Spiel noch weiter programmieren, beispielsweise mit einer grafischen Oberfläche versehen, dann hätten wir noch viel mehr Methoden zu implementieren – ziemlich alle wohl mit der Variablen zuege in der Liste der Übergabeparameter. Irgendwann fängt so eine Art Wiederholung an zu nerven. Dieses Phänomen ist sehr charakteristisch für prozedurale Programmierung, also Software, die nur mit isolierten Prozeduren arbeitet.

Irgendjemand hat sich irgendwann einmal überlegt, das zu ändern und die objektorientierte Programmierung erfunden. Das ist schon ziemlich lange her. Die Frage ist somit durchaus berechtigt, warum ich dir dies nicht von Anfang an erzählt habe und wir uns bis jetzt ausschließlich mit Prozeduren (oder Methoden) abgegeben haben?

Meine Antwort darauf: Man kapiert objektorientierte Programmierung einfach nicht von Anfang an! Das ging mir bei meinen ersten Programmierübungen so und das wird bei dir nicht anders sein. Über objektorientierte Programmierung zu sprechen lohnt sich wirklich erst, wenn man mit prozeduraler Programmierung sich einen Teil einer Programmiersprache erobert hat. Potenzrechnen versteht man erst, wenn man die Multiplikation verstanden hat, und diese wiederum erst, wenn Addition kein Fremdwort mehr ist. Also alles schön der Reihe nach.

Was ist nun aber objektorientierte Programmierung genau?

Schauen wir wieder auf unser Spiel Tic-Tac-Toe. Die Liste der zuege sind die wichtigsten Daten des Spiels. Die 14 von ihr abhängigen Methoden arbeiten immer mit oder auf diesen Daten. Die objektorientierte Programmierung bietet die Möglichkeit, solche Daten und ihre abhängigen Methoden dicht zusammenzuhalten (man sagt auch kapseln). In der prozeduralen Programmierung ist die Datei selbst das einzige Mittel, Daten und Methoden zusammenzuhalten. Das erste wichtige Merkmal der objektorientierten Programmierung (das schreibe ich jetzt zum letzten Mal aus, ab sofort verwende ich die gebräuchliche Abkürzung OOP) ist somit Kapselung.

Für das Spiel Tic-Tac-Toe könnte das etwa so aussehen:


class TicTacToe

  def initialize()
    @zuege = []
  end

  def spielfeld(out)
    # ...
  end

  def print_zeile(out, zeile)
    # ...
  end

  def print_feld(spalte, zeile)
    # ...
  end

  # ... restliche Methoden
end

Zwischen dem beginnenden class und dem zugehörigen end erfolgt die Kapselung von Daten (die zuege) und der Methoden, die auf den Daten arbeiten.

Warum aber erfolgt die Kapselung bei OOP mit dem Schlüsselwort class und nicht objekt (oder englisch object)? Wäre es nicht einleuchtender, wenn wir es so schreiben würden:


object TicTacToe

  def initialize()
    @zuege = []
  end

  # ... restliche Methoden
end

Zwischen einer Klasse (englisch class) und einem Objekt (englisch object) gibt es in OOP einen feinen Unterschied: Die Klasse ist der Bauplan der Objekte! Stelle es dir vor wie ein Architekt, der eine ganze Wohnsiedlung mit einem einzigen Haustyp baut. Er hat nur einen Bauplan eines Hauses. Mit diesem einen Bauplan kann er aber soviele Häuser bauen wie er möchte. Alle Häuser sehen gleich aus. Naja, nicht wirklich gleich. Das eine hat vielleicht rote Fensterläden, das andere grüne. Aber der Haustyp ist bei allen gleich, weil ihnen allen derselbe Bauplan zugrunde liegt.

Das OO Programmieren besteht also darin, Baupläne zu entwerfen. Den späteren Bau der Objekte sieht man nicht wirklich, der erfolgt nach diesen Bauplänen dann vom Rubyinterpreter. Lass dir ruhig etwas mit diesem Gedanken Zeit, wenn du es nicht sofort verstehst. Das ist normal. Mit jedem Stück OO Code wirst du es besser verstehen.

Schauen wir uns noch die anderen Besonderheiten der neuen Schreibweise an.

Konstruktor oder Geburtshelfer

In der Klasse TicTacToe haben wir eine Methode initialize definiert (abgeleitet vom lateinischen initio – anfangs), die wir bisher überhaupt nicht benötigten. Jede Klasse kann diese Methode besitzen, muss aber nicht. Initialize hat eine besondere Bedeutung: sie enthält die Anweisungen, die ganz am Anfang ausgeführt werden, wenn ein Objekt der Klasse das Licht der Welt erblickt. Daher nennt man diese Methode auch den Konstruktor der Klasse, also der Bereich, der dem Objekt alles für einen guten Start ins rauhe OO-Leben verpasst.


# Klasse definieren
class TicTacToe
  def initialize()
    puts "Hurra, ich bin da!" 
  end
end

# Ein Objekt der Klasse anlegen
spiel = TicTacToe.new

Führst du obiges Programm aus, erscheint als Ausgabe der Text Hurra, ich bin da! auf der Kommandozeile.

Die Methode initialize wird nie direkt aufgerufen, sondern indirekt von der Methode new. Innerhalb dieser Methode new wird irgendwann dann die Methode initialize aufgerufen und danach ist das Objekt für den weiteren Gebrauch fertig.

Wo aber kommt die Methode new denn nun schon wieder her, die haben wir doch nirgends definiert!

Methoden von Objekten und Klassen

Die Methode new ist eine weitere besondere Methode, die Ruby jeder Klasse automatisch verpasst. Jeder Klasse, sage ich! Nicht jedem Objekt der Klasse!

Eine Klassenmethode gibt es für alle Objekte einer Klasse immer nur ein einziges mal. Eine Objektmethode hat jedes Objekt dagegen für sich. Du erkennst den Unterschied daran, dass einer Klassenmethode der Name der Klasse vorangestellt wird, während vor einer Objektmethode der Name des Objekts steht—jeweils mit einem Punkt vom Methodennamen abgetrennt.

Attribute

Die Variable zuege ist aus allen Methoden verschwunden. Stattdessen steht sie direkt im Konstruktor mit einem @-Zeichen davor. Das @ markiert diese Variable als ein Attribut. Attribute sind die Variablen einer Klasse, die die Daten enthalten. Man nennt die Attribute auch Eigenschaften der Klasse. Für den außenstehenden sind diese Attribute nicht zugänglich, sie sind in der Klasse versteckt, verkapselt.

Beispiel für OOP

Am Beispiel der Klasse Haus wollen wir uns das bisher gehörte etwas genauer anschauen:


# Definition der Klasse 'Haus'
class Haus
  def initialize(f, fd)
    @farbe      = f
    @farbe_dach = fd
  end
  def to_s
    "Haus[farbe=#{@farbe}, farbe_dach=#{@farbe_dach}]" 
  end
end

# Anlegen von zwei Objekten der Klasse 'Haus'
h1 = Haus.new("gelb",  "rot")
h2 = Haus.new("braun", "rot")

puts h1.to_s
puts h2.to_s

Der Konstruktor wird von außen mit zwei Variablen aufgerufen: die Farbe des Hauses und die Farbe des Daches. Diese Angaben merkt sich jedes Objekt in seinen Attributen @farbe und @farbe_dach. Wird die Methode to_s für ein Objekt aufgerufen, so erhält man eine Zeichenkette, die diese Werte dieser Attribute enthält.


C:\entwicklung> ruby lektion_16.rb
Haus[farbe=gelb, farbe_dach=rot]
Haus[farbe=braun, farbe_dach=rot]

Du solltest nach dieser Lektion heute nicht einfach zur nächsten weitergehen, solange du die beiden Zeilen


puts h1.to_s
puts h2.to_s

noch nicht verstanden hast. Dir muss klar werden, warum in beiden Zeilen zwar dieselbe Methode to_s aufgerufen wird, aber beide Aufrufe nicht dasselbe Ergebnis liefern.

Es liegt daran, dass es sich hier eben nicht mehr um dieselben Methoden handelt! Es sind im Grunde zwei verschiedene Methoden, denen nur derselbe Bauplan aus der Klassendefinition zugrunde liegt.

Die erste Methode to_s gehört zum Objekt h1, die zweite zum Objekt h2. Beide Objekte haben unterschiedliche Attribute, d.h. beide Häuser haben unterschiedliche Farben. Somit liefern beide Methoden unterschiedliche Ergebnisse, obwohl sie denselben Namen haben. Eine Objektmethode arbeitet immer auf den Daten des jeweiligen Objektes.

Belassen wir es für heute dabei. Du solltest erkannt haben, wie sich OOP von dem prozeduralen Programmieren unterscheidet.

Lektion 15 - Tic-Tac-Toe, Computer unschlagbar? 2

Erstellt von Frithjof Thu, 25 Oct 2007 23:03:00 GMT

Dein Computer verliert noch häufig beim Tic-Tac-Toe. Das wollen wir mit dieser Lektion ändern. Du machst ihn zu einem stärkeren Gegner, der bei der Wahl seiner Züge auch den aktuellen Spielstand mit berücksichtigt. Wo er mit dem nächsten Zug gewinnen kann, wird er es tun, andernfalls sucht er sich gute Felder für seine Züge aus.

In Lektion 14 führten wir die Methode computer_zug ein. Zunächst mit den beiden Strategien naiver Zug und Zufallszug (die man eigentlich gar nicht als Strategien bezeichnen kann). An dieser Stelle fügst du heute eine neue Strategie intelligenter_zug hinzu. Innerhalb dieser Methode werden wir einige der Spielstrategien implementieren, die den Computer (fast) unschlagbar machen.

Tic-Tac-Toe ist ein aus Sicht des Computers recht einfaches Spiel, verglichen etwa mit Schach. Theoretisch wäre es für den Computer möglich, zu jedem aktuellen Spielstand den optimalen Zug zu wählen, der ihn vor dem Verlieren bewahrt, also höchstens für ihn mit einem Unentschieden endet. Dazu müsste er alle Spielausgänge berechnen, die vom aktuellen Spielstand aus möglich sind, je nachdem, welcher Zug als nächstes gewählt werden würde. Allerdings ist dieses Vorgehen nicht ganz so einfach zu implementieren wie es sich anhört. Und gerade weil Tic-Tac-Toe ein recht einfaches Spiel ist, reicht es aus, ein paar Regeln zu implementieren, die den Computer ausreichend stark machen. Wir werden folgende Regeln implementieren:

  1. Kannst du 3 Steine in einer Reihe setzen, dann tue es.
  2. Kann dein Gegner 3 Steine in einer Reihe setzen, dann verhindere es indem du deinen Stein in diese Reihe setzt.
  3. Ist die Mitte noch frei, dann setzte in die Mitte.
  4. Hat der Gegner einen Stein in einer Ecke und ist die gegenüberliegende Ecke noch frei, dann setze in diese freie Ecke.
  5. Ist eine Ecke frei, dann setze in die Ecke.

Ist der Computer an der Reihe, geht er alle Regeln in obiger Reihenfolge durch und wählt entsprechend der ersten zutreffenden Regel seinen Zug. Hat er sich am Ende der letzten Regel immer noch nicht für einen Zug entscheiden können, so macht er schließlich einen Zufallszug.

Regel 1: Gewinne!

Hier ist die Methode intelligenter_zug mit der Implementierung der ersten Regel. Der Computer versucht zu gewinnen. Falls das nicht geht, macht er einen Zufallszug.

Um zu entscheiden, ob er überhaupt gewinnen kann, muss er für jede Reihe nachschauen, ob dort Felder von ihm besetzt sind, ob es genau 2 sind und ob das dritte Feld frei ist (also nicht schon vom Gegner besetzt ist). Das Ermitteln der besetzten und freien Felder einer Reihe übernimmt die Methode reihen_status. Sie liefert die Liste der freien Felder in einer Reihe (maximal 3 Felder können in einer Reihe frei sein) und einen Hash, der für jeden Spieler eine Liste mit den von ihm besetzten Feldern in der Reihe festhält.


def computer_zug(zuege, spieler, wer)
  #naiver_zug(zuege, spieler, wer)
  #zufalls_zug(zuege, spieler, wer)
  intelligenter_zug(zuege, spieler, wer)
end

# Implementiert einige Regeln, mit deren Hilfe man sehr wahrscheinlich
# nicht verliert.
#  zuege   - Liste der Züge
#  spieler - Liste mit beiden Spielern
#  wer     - Index in der Spielerliste, der angibt, wer gerade am Zug ist
def intelligenter_zug(zuege, spieler, wer)
  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],
  ]

  zug = nil

  # 1. Regel: Zuerst nach einer Gewinnsituation suchen
  for reihe in reihen
    besetzt, frei = reihen_status(zuege, reihe)

    # Wenn der aktuelle Spieler in einer Reihe bereits zwei Felder
    # besetzt hält und das dritte frei ist, dann natürlich das nehmen
    if (frei.size == 1) and (besetzt[spieler[wer][0]].size == 2)
      zug = [spieler[wer][0], nummer_in_spalte_zeile(frei[0])].flatten
      break # nicht weitersuchen
    end
  end

  # Andernfalls Zufallszug machen
  if zug.nil?
    zug = zufalls_zug(zuege, spieler, wer)
  end

  zug
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(zuege, reihe)
  # Welche Felder sind noch frei?
  frei_alle = freie_felder(zuege)
  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 = {:o => [], :x => []}
  for zug in zuege
    # Liegt der zug in der fraglichen Reihe?
    feld = nummer_aus_spalte_zeile(zug[1], zug[2])
    if reihe.include?(feld)
      # Wer besetzt es?
      if zug[0] == :x
        # X besetzt das Feld, nehme das Feld in die Besetztliste von X auf
        besetzt[:x] << feld
      elsif zug[0] == :o
        # O besetzt das Feld, nehme das Feld in die Besetztliste von O auf
        besetzt[:o] << feld
      end
    end
  end
  [besetzt, frei]
end

Regel 2: Lass den Gegner nicht gewinnen!

Die zweite Regel versucht zu verhindern, dass der Gegner in seinem nächsten Zug eine Reihe mit seinen 3 Steinen vervollständigt und gewinnt. Im Prinzip ist hier genau das gleiche zu tun wie in Regel 1 nur mit dem Unterschied, dass die Prüfung der zwei besetzten Felder natürlich für den Gegner erfolgt.


...
  if zug.nil?
    # 2. Regel: Suche dann nach den Reihen, in denen der Gegner bereits
    # genau 2 Felder besetzt hat und das dritte Feld noch frei ist.
    for reihe in reihen
      besetzt, frei = reihen_status(zuege, reihe)

      # Gefährlich, wenn Gegner zwei besetzt hält. Wie in der vorherigen
      # Lektion gelernt, erhält man zum Index des aktuellen Spielers
      # in der Spielerliste den Index des Gegners mit der Bitoperation 1^wer
      if (frei.size == 1) and (besetzt[spieler[1^wer][0]].size == 2)
        # Jetzt muss der Spieler unbedingt das eine freie Feld besetzen!
        # Andernfalls kann der Gegner im nächsten Zug gewinnen.
        zug = [spieler[wer][0], nummer_in_spalte_zeile(frei[0])].flatten
        break # nicht weitersuchen
      end
    end
  end
...

Regel 3: Spiel die Mitte!

Die Mitte ist immer das Feld mit der Nummer 5. Das ist also ziemlich einfach zu implementieren. Wenn Feld 5 frei ist, dann nimmt der Computer dieses.


...
  # 3. Regel: Immer in die Mitte setzten, falls dort frei ist
  if zug.nil?
    frei  = freie_felder(zuege)
    mitte = 5
    if frei.include?(mitte)
      zug = [spieler[wer][0], nummer_in_spalte_zeile(mitte)].flatten
    end
  end
...

Regel 4: Spiel die gegenüberliegende Ecke!

Am kompliziertesten scheint es, eine dem Gegner gegenüberliegende freie Ecke zu identifizieren.

Hier bestimmen wir zunächst alle Ecken, die vom Gegner besetzt werden und merken uns dies für jede Ecke (Felder 1, 3, 7 oder 9) in einem Hash, wobei 0 bedeuten soll, die Ecke ist frei, und 1 bedeutet sie ist vom Gegner besetzt.

Dann prüfen wir für alle besetzten Ecken, ob die gegenüberliegende Ecke frei ist und wählen die erste dieser Ecken für den Computer als seinen Zug aus.


...
  # 4. Regel: Verteidige gegenüberliegende Ecke
  frei  = freie_felder(zuege)
  ecken = {
    1 => 0, # links oben
    3 => 0, # rechts oben
    7 => 0, # links unten
    9 => 0  # rechts unten
  }
  for z in zuege
    feld = nummer_aus_spalte_zeile(z[1], z[2])
    # Gegner besetzt die Ecke, wenn:
    #   feld ist eine Ecke  und  Gegner besetzt sie
    if (ecken[feld] != nil) and (z[0] == spieler[1^wer][0])
      # Markiere Ecke als vom Gegner besetzt
      ecken[feld] = 1
    end
  end

  if zug.nil?
    # Wenn Ecke 1 besetzt, dann setze auf 9, oder umgekehrt (sofern frei).
    # Wenn Ecke 3 besetzt, dann setze auf 7, oder umgekehrt (sofern frei).
    gegen_ecken = [[1, 9], [9, 1], [3, 7], [7, 3]]
    for ecken_paar in gegen_ecken
      if (ecken[ecken_paar[0]] > 0) and (frei.include?(ecken_paar[1]))
        zug = [spieler[wer][0], nummer_in_spalte_zeile(ecken_paar[1])].flatten
        break # nicht weitersuchen
      end
    end
  end
...

Regel 5: Spiel eine Ecke!

Die letzte Regel sucht in allen Ecken nach der ersten freien und verwendet diese für den Zug des Computers.


...
  # 5. Regel: Setze in irgendeine freie Ecke.
  # Verwende Variablen 'frei' und 'ecken' von oben
  if zug.nil?
    for ecke in ecken.keys
      if frei.include?(ecke)
        zug = [spieler[wer][0], nummer_in_spalte_zeile(ecke)].flatten
        break # nicht weitersuchen
      end
    end
  end
...

Hier nochmals die gesamte Methode intelligenter_zug:


# Implementiert einige Regeln, mit deren Hilfe man sehr wahrscheinlich
# nicht verliert.
#  zuege   - Liste der Züge
#  spieler - Liste mit beiden Spielern
#  wer     - Index in der Spielerliste, der angibt, wer gerade am Zug ist
def intelligenter_zug(zuege, spieler, wer)
  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],
  ]

  zug = nil

  # 1. Regel: Zuerst nach einer Gewinnsituation suchen
  for reihe in reihen
    besetzt, frei = reihen_status(zuege, reihe)

    # Wenn der aktuelle Spieler in einer Reihe bereits zwei Felder
    # besetzt hält und das dritte frei ist, dann natürlich das nehmen
    if (frei.size == 1) and (besetzt[spieler[wer][0]].size == 2)
      zug = [spieler[wer][0], nummer_in_spalte_zeile(frei[0])].flatten
      break # nicht weitersuchen
    end
  end

  if zug.nil?
    # 2. Regel: Suche dann nach den Reihen, in denen der Gegner bereits
    # genau 2 Felder besetzt hat und das dritte Feld noch frei ist.
    for reihe in reihen
      besetzt, frei = reihen_status(zuege, reihe)

      # Gefährlich, wenn Gegner zwei besetzt hält. Wie in der vorherigen
      # Lektion gelernt, erhält man zum Index des aktuellen Spielers
      # in der Spielerliste den Index des Gegners mit der Bitoperation 1^wer
      if (frei.size == 1) and (besetzt[spieler[1^wer][0]].size == 2)
        # Jetzt muss der Spieler unbedingt das eine freie Feld besetzen!
        # Andernfalls kann der Gegner im nächsten Zug gewinnen.
        zug = [spieler[wer][0], nummer_in_spalte_zeile(frei[0])].flatten
        break # nicht weitersuchen
      end
    end
  end

  # 3. Regel: Immer in die Mitte setzten, falls dort frei ist
  if zug.nil?
    frei  = freie_felder(zuege)
    mitte = 5
    if frei.include?(mitte)
      zug = [spieler[wer][0], nummer_in_spalte_zeile(mitte)].flatten
    end
  end

  # 4. Regel: Verteidige gegenüberliegende Ecke
  frei  = freie_felder(zuege)
  ecken = {
    1 => 0, # links oben
    3 => 0, # rechts oben
    7 => 0, # links unten
    9 => 0  # rechts unten
  }
  for z in zuege
    feld = nummer_aus_spalte_zeile(z[1], z[2])
    # Gegner besetzt die Ecke, wenn:
    #   feld ist eine Ecke  und  Gegner besetzt sie
    if (ecken[feld] != nil) and (z[0] == spieler[1^wer][0])
      # Markiere Ecke als vom Gegner besetzt
      ecken[feld] = 1
    end
  end

  if zug.nil?
    # Wenn Ecke 1 besetzt, dann setze auf 9, oder umgekehrt (sofern frei).
    # Wenn Ecke 3 besetzt, dann setze auf 7, oder umgekehrt (sofern frei).
    gegen_ecken = [[1, 9], [9, 1], [3, 7], [7, 3]]
    for ecken_paar in gegen_ecken
      if (ecken[ecken_paar[0]] > 0) and (frei.include?(ecken_paar[1]))
        zug = [spieler[wer][0], nummer_in_spalte_zeile(ecken_paar[1])].flatten
        break # nicht weitersuchen
      end
    end
  end

  # 5. Regel: Setze in irgendeine freie Ecke.
  # Verwende Variablen 'frei' und 'ecken' von oben
  if zug.nil?
    for ecke in ecken.keys
      if frei.include?(ecke)
        zug = [spieler[wer][0], nummer_in_spalte_zeile(ecke)].flatten
        break # nicht weitersuchen
      end
    end
  end

  # Andernfalls Zufallszug machen
  if zug.nil?
    zug = zufalls_zug(zuege, spieler, wer)
  end

  zug
end

Peter und Livia

Peter: Was bedeutet das komische flatten?

...
  zug = [spieler[wer][0], nummer_in_spalte_zeile(ecke)].flatten
...

Livia: Das ist ein Befehl für eine Liste (oder Array) und bedeutet zu deutsch verflachen. Damit kannst du eine verschachtelte Liste (eine Liste, die andere Listen als Elemente enthält) zu einer einzigen Liste verflachen, die nur noch Elemente enthält, die selbst keine Listen sind.

Zum Beispiel ist liste eine Liste mit 4 Elementen, von denen das vierte Element selbst wieder eine Liste aus 4 Elemente ist, bei der wiederum das vierte Element eine Liste, nun aber nur mit 3 Elementen ist:


...
  liste  = [1, 2, 3, [eins, zwei, drei, ["Sonne", "Mond", "Sterne"]]]
  liste_flach = liste.flatten
...

Die verflachte Liste liste_flach besteht nun also aus 9 Elementen:


...
  [1, 2, 3, eins, zwei, drei, "Sonne", "Mond", "Sterne"]
...

Lektion 14 - Tic-Tac-Toe gegen den Computer

Erstellt von Frithjof Tue, 16 Oct 2007 20:24:00 GMT

Dein Computer gehorcht dir bisher prima bei der Ausführung des Rubyprogramms Tic-Tac-Toe. Er fühlt sich aber etwas einsam, weil er nicht wirklich mitspielen darf. Das wollen wir in dieser Lektion ändern – der Computer darf Tic-Tac-Toe spielen! Er wird in dieser Lektion aber noch kein Profi werden. Wir sind zufrieden, wenn er überhaupt mitspielt, auch wenn man ihn noch sehr gut besiegen kann.

Schauen wir uns an, wo wir den Computer als Gegner mit einbeziehen können. Die Methode play_2_spieler sorgt bisher dafür, dass zwei Spieler miteinander spielen können. Der Methode ist aber eigentlich ziemlich egal, ob die Eingaben für die Züge von einem menschlichen Spieler kommen, oder von einem Computerprogramm bestimmt werden. Du definierst dir also eine weitere Methode play_gegen_computer und änderst sie in geeigneter Weise ab.


# Lässt 1 Spieler gegen den Computer spielen
def play_gegen_computer(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  ergebnis = [false, nil]

end

Die Methode play_gegen_computer erhält beim Aufruf neben der Aus- und Eingabe die Liste der (noch leeren) Züge. Sie definiert dann die beiden Spieler in der Variablen spieler und das Ergebnis in der Variablen ergebnis wie bisher auch.

Es fehlt noch die Variable wer, die den aktuellen Spieler festlegt bzw. am Anfang den Spieler bestimmt, der mit dem Spiel beginnen darf. Beim Spiel gegen den Computer müssten wir festlegen, mit welchen Steinen (X oder O) der Computer spielen soll. Du fügst dafür eine Abfrage an den menschlichen Spieler ein und fragst ihn, mit welchem Stein er spielen möchte. Den anderen nimmt dann zwangsläufig der Computer. Wir legen außerdem fest, dass der menschliche Spieler immer anfangen darf.


# Lässt 1 Spieler gegen den Computer spielen
def play_gegen_computer(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  ergebnis = [false, nil]

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

  out.puts "Los geht's! Du spielst #{spieler[wer][1]}, und ich #{spieler[1^wer][1]}!" 
  out.puts "Du fängst an." 
end
Das Abfragen nach dem Spielstein erfolgt also in der Zeile:

  ...
  eingabe = ein.gets.downcase.chomp
  ...

ein ist die Eingabekonsole, auf der der menschliche Spieler etwas eintippt, gets holt sich die eingetippte Zeichenkette, downcase verwandelt die Eingabe in Kleinbuchstaben und chomp schneidet das unsichtbare Zeichen für die Entertaste ab.

Bis jetzt weißt du im Rubyprogramm aber immer noch nicht, ob der Spieler nun X (oder x) oder O (oder o) eingegeben hat. Das findest du erst mit der nächsten Zeile heraus:


  ...
  eingabe = ein.gets.downcase.chomp
  wer = (eingabe == 'x') ? 1 : 0
  ...

Mit dem ternären Operator entscheidest du, mit welchem Stein der menschliche Spieler nun tatsächlich anfängt. Hat er X (oder x) eingetippt, dann erhält er den Stein X, in allen anderen Fällen, also egal ob er O (oder o) oder irgendeine andere Zeichenkette eingetippt hat, erhält er den Stein O.

Na prima, jetzt wissen wir, mit welchem Stein der menschliche Spieler beginnt und können nun in die Spielschleife eintreten. Dort darf der menschliche Spieler zuerst seinen Zug eingeben, der Zug wird gemacht und das neue Spielfeld ausgeben. Danach macht sofort der Computer weiter mit seinem Zug. Und hier wird es dann spannend. Schauen wir uns also die fertige Methode play_gegen_computer an:


# Lässt 1 Spieler gegen den Computer spielen
def play_gegen_computer(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  ergebnis = [false, nil]

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

  out.puts "Los geht's! Du spielst #{spieler[wer][1]}, und ich #{spieler[1^wer][1]}!" 
  out.puts "Du fängst an." 

  while true
    # Der menschliche Spieler zuerst
    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

    # Gleich den Zug des Computers anschließen
    zug_okay = false
    until zug_okay
      out.puts "\nJetzt bin ich dran!" 
      zug = computer_zug(zuege, wer)
      out.puts "Ich setze auf das Feld #{nummer_aus_spalte_zeile(zug[1], zug[2])}." 
      break if zug.nil?
      zug_okay = zug_hinzu(spieler[wer][0], zug[1], zug[2], 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

Der Rubycode für das Durchführen des Computerzuges ähnelt dem für den menschlichen Spieler ziemlich. Aber es gibt doch einige Unterschiede.

Die entscheidende Stelle ist


  ...
  zug = computer_zug(zuege, wer)
  ...

Hier wird eine Methode computer_zug aufgerufen, die die aktuelle Liste der Züge und den aktuellen Spielstein des Computers erhält. In dieser Methode kannst du dann eine beliebige Spielstrategie des Computers entwickeln. Ich zeige dir gleich zwei mögliche einfache Strategien, aber zunächst nehmen wir mal an, die Methode liefert uns einen Zug in der Form einer kleinen Liste


  [wer, spalte, zeile]

Der Computerzug liefert uns also nicht die Feldnummer, sondern gleich die Spalte und Zeile für die Platzierung des Spielsteins. Die Feldnummer ist ja vornehmlich zur erleichterten Eingabe für den menschlichen Spieler gedacht gewesen.

Nachdem der Computerzug erfolgreich hinzugefügt, das Spielfeld erneut ausgegeben und der nächste Spieler bestimmt wurde, ist der menschliche Spieler wieder am Zug, sofern das Spiel noch nicht vorbei ist (keine freien Felder mehr, oder jemand hat gewonnen).

Spielstrategien für den Computer

Schauen wir uns also nun wie besprochen ein paar Spielstrategien für den Computer an.


def computer_zug(zuege, wer)
  naiver_zug(zuege, wer)
end

def naiver_zug(zuege, wer)
  frei = freie_felder(zuege)
  zug = nil
  if frei.size > 0
    # Nehme das erste freie Feld
    spalte, zeile = nummer_in_spalte_zeile(frei[0])
    zug = [wer, spalte, zeile]
  end
  zug
end

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

Der Naive Zug

Die Methode computer_zug ruft die Methode naiver_zug auf, die sich ziemlich dumm anstellt. Sie nimmt nämlich einfach das nächste freie Feld. Dabei geht sie wie folgt vor.

Sie bestimmt zuerst die freien Felder mit Hilfe der Methode freie_felder.

  ...
  frei = freie_felder(zuege)
  ...
Wenn es mindestens ein freies Feld gibt, beschafft sie sich die Feldnummer des ersten Feldes, daraus die Spalte und Zeile und bildet damit die kleine Liste, die den Zug beschreibt.

  ...
  if frei.size > 0
    # Nehme das erste freie Feld
    spalte, zeile = nummer_in_spalte_zeile(frei[0])
    zug = [wer, spalte, zeile]
  end
  ...

Was meinst du, wie oft der Computer mit dieser Strategie gewinnt? Ich denke kaum, der menschliche Spieler braucht sich nicht sehr anzustengen, um zu gewinnen. Außerdem würde der menschliche Spieler nach ein paar Runden bemerken, dass der Computer immer auf das erst beste freie Feld setzt.

Wir brauchen eine andere Strategie.

Der Zufallszug

Damit der menschliche Spieler nicht so leicht die Spielstrategie des Computers durchschaut, müssen wir ein wenig Zufall mit ins Spiel bringen. Das ist zwar auch noch keine wirklich gute Strategie für den Computer, um zu gewinnen, aber immerhin, tut er wenigstens so.

Du kommentierst also zunächst den Aufruf der naiven Methode aus und rufst die neue Methode zufalls_zug auf.

Moment mal! Aber wie soll der Computer eine zufällige Entscheidung treffen? Soll er seine Augen schließen und mit dem Finger auf irgendein (freies) Feld zeigen? Wie geht das? Du hast bisher keinen Rubybefehl für den Zufall kennen gelernt. Im richtigen Leben gibt es viele Möglichkeiten, den Zufall entscheiden zu lassen: man wirft eine Münze, wer vorher auf Kopf gesetzt hat, gewinnt genau dann, wenn die Münze mit der Kopfseite oben zu liegen kommt. Oder du hast Stöckchen, von denen eines besonders kurz ist. Wer dieses zieht, gilt als der vom Zufall auserwählte.

Aber das hilft uns alles nicht weiter. Das Problem ist die Entscheidung für einen von mehreren möglichen Werten. Angenommen wir haben eine Liste der freien Felder vom aktuellen Spielstand. Der Computer müsste sich für einen Index in der Liste entscheiden. Beim naiven Zug nahm er immer den ersten Index 0. Wie bringen wir ihn dazu, mal den Index 0, mal den Index 3 oder 9 zu nehmen?

Ich zeige dir im folgenden meine Idee. Du kannst dir aber gerne auch eine andere Lösung überlegen. Der Zufallszug hier funktioniert so:


def computer_zug(zuege, wer)
  #naiver_zug(zuege, wer)
  zufalls_zug(zuege, wer)
end

def zufalls_zug(zuege, wer)
  frei = freie_felder(zuege)
  zug = [wer, 0, 0]
  if frei.size > 0
    jetzt = Time.now
    sekunden = jetzt.sec
    index = sekunden % frei.size
    spalte, zeile = nummer_in_spalte_zeile(frei[index])
    zug = [wer, spalte, zeile]
  end
  zug
end
  1. Der Computer (das heißt das Rubyprogramm) bestimmt zuerst wieder die freien Felder.
  2. Dann schaut es nach der aktuellen Uhrzeit! Heh? Was hat die Uhrzeit mit dem Zufall zu tun? Die Idee ist, bei der Uhrzeit nur auf die Sekunden zu achten. Wenn du zu einem beliebigen Zeitpunkt auf deine Uhr schaust, ist es sehr unwahrscheinlich, dass du zweimal dieselbe Sekundenanzeige siehst. Erst wenn du genau nach einer Minute wieder auf die Uhr schaust, wird die selbe Sekunde angezeigt.
  3. Der Computer bestimmt also aus der aktuellen Uhrzeit nur die Sekunden.
  4. Dann teilt der die Sekunden durch die Anzahl der freien Felder und merkt sich von dieser Division nur den Rest. Der Rest ist immer kleiner als das wodurch man dividiert (Divisor).
  5. Somit kann der Computer diesen Rest als Index für die Liste der freien Felder ansehen.

Der Computer hat nun eine sehr einfache Möglichkeit, eine Zufallsentscheidung zwischen mehreren Möglichkeiten zu treffen. Es reicht allein die Uhrzeit.

Probiere die beiden Strategien, die naive und die mit Zufall, ein wenig aus, indem du abwechselnd eine Runde spielst und dabei jeweils die entsprechende Zeile in der Methode computer_zug aus- bzw. einkommentierst.

Bei dem Zufallszug hast du als menschlicher Spieler das Gefühl, der Computer denkt sich etwas bei seinen Zügen, weil du kein Muster erkennst. In Wahrheit denkt er sich natürlich überhaupt nichts. Es ist immer noch keine gute Strategie für ihn, um oft zu gewinnen.

Du kannst ja gerne eigene Strategien entwickeln. In der nächsten Lektion schauen wir uns gemeinsam eine bessere an. Aber für heute ist es denke ich genug.

Peter und Livia

Peter: Mir ist in der Methode play_gegen_computer folgende Zeile aufgefallen.

  ...
  out.puts "Los geht's! Du spielst #{spieler[wer][1]}, und ich #{spieler[1^wer][1]}!" 
  ...
Da verstehe ich nicht, was das mit dem kleinen Dach ^ zu bedeuten hat?

Livia: Das ist ein Bit-Operator. Er heißt XOR, was eXklusives OdeR bedeutet, also entweder oder. 1^wer wird also 1, nur dann, wenn wer gleich 0 ist. Es wird 0 genau dann und nur dann, wenn wer 1 ist. Somit kann man leicht zu einem gegebenen Spieler immer den anderen Spieler angeben.

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.

Lektion 12 - Tic-Tac-Toe, Eingabe von Zügen

Erstellt von Frithjof Sun, 16 Sep 2007 21:50:00 GMT

Tic-Tac-Toe hast du nun schon soweit implementiert, dass das Spielbrett auf der Konsole erscheint und du auch über den Aufruf einer Methode Züge hinzufügen kannst. Wir machen in dieser Lektion das Spiel interaktiv. Ruby sagt dir, wer als nächstes dran ist und wartet auf die Eingabe des Spielers. Das Eingeben von Werten, die das Rubyprogramm dann weiterverwendet, hast du ja schon in der Lektion 7 ausprobiert, wo du ein Fahrrad über die Tastatur steuern konntest. Aber alles schön der Reihe nach, denn vorher schaffst du noch etwas Ordnung in das Chaos der letzten Lektion.

Programme gliedern

In der Lektion 11 hast du genau vier Methoden entwickelt: spielfeld, print_zeile, print_feld und zug_hinzu. Unterhalb der Methoden geht dann das eigentliche Programm mit dem Erzeugen der Variablen zuege für die Liste der Züge und dem Aufruf des Spielfeldes los. Im Verlauf deiner Arbeiten an dem Spiel Tic-Tac-Toe werden noch einige Methoden hinzu kommen. Auch wird das eigentliche Programm unterhalb der Methoden umfangreicher werden. Den Überblick zu behalten wird zunehmend schwieriger. Dagegen werden wir folgendes tun:
  1. Du legst eine neue Datei mit Namen tictactoe.rb an. In dieser Datei wirst du alle Methoden verwalten.
  2. Das eigentliche Programm behältst du in einer anderen Datei, zum Beispiel für diese Lektion in der Datei lektion_12.rb. In dieser Datei machst du die Methoden aus tictactoe.rb über den require Befehlt bekannt.

So sollte es nach dem Umorganisieren bei dir auch aussehen:

Datei tictactoe.rb


# 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)
  # 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

Datei lektion_12.rb


# lektion_12.rb

# Bekanntmachen der Methoden aus tictactoe.rb
require 'tictactoe'

# 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)

Ab jetzt gilt: Neue Methoden gehören nur noch in die Datei tictactoe.rb, das Programm selbst erweiterst du nur noch in der Datei lektion_12.rb (in den nächsten Lektionen natürlich entsprechend).

Eingabe von Zügen

Genug der Vorarbeiten, jetzt können wir uns der eigentlichen Aufgabe dieser Lektion widmen. Zunächst ändern wir das Programm in lektion_12.rb wie folgt:


# lektion_12.rb

require 'tictactoe'

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

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

play_2_spieler(STDOUT, STDIN, zuege)

Nach dem Anlegen der leeren Liste für die Züge und der ersten Ausgabe des noch leeren Spielfeldes, rufen wir die Methode play_2_spieler auf. Neu ist hier lediglich die Variable mit dem Namen STDIN. In der letzten Lektion hast du gelernt, dass STDOUT der Name für die Standardausgabe, also die Konsole ist, von wo aus das Programm gestartet wurde. Dann ist natürlich STDIN der Name der Standardeingabe, also ebenfalls der Konsole, von wo aus das Programm gestartet wurde. Von uns aus gesehen handelt es sich zwar immer um dieselbe Konsole, Ruby braucht aber zwei verschiedene Eimer für die Ein- bzw. Ausgabe. Ruby kann nicht in ein und denselben Eimer etwas ausgeben und gleichzeitig aus diesem etwas lesen. Die Namen STDIN und STDOUT sind übrigens die Namen der Griffe der beiden Eimer (engl. handle). Das Betriebssystem (bspw. Windows XP oder Linux) sorgen dann dafür, dass aber sowohl die Ausgaben als auch die Eingaben auf derselben Konsole erscheinen.

Mehr ist hier im Hauptprogramm zunächst nicht zu tun. Machen wir nun weiter in der Datei tictactoe.rb, in der wir ja von nun an alle Methoden verwalten wollen. Du legst in tictactoe.rb die Methode play_2_spieler wie folgt an:


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
end
Die Methode kennt drei Variablen out, ein und zuege, die ihr von außen vom Aufrufer mit übergeben werden. Als erstes definieren wir die beiden Spieler in der Liste spieler. Ein Spieler besteht wiederum aus einer Liste mit 2 Elementen:
  • Das erste Element ist das Symbol, das wir intern zur Unterscheidung der Spieler verwenden, also :o für den Spieler O und :x für den Spieler X.
  • Das zweite Element ist das Zeichen, das wir für die Ausgabe verwenden, also die Großbuchstaben O und X.
Was soll die Methode eigentlich genau machen? Ich schlage folgendes vor:
  1. Sie wählt den Spieler aus, der als nächstes dran ist (oder am Beginn einen, der anfängt).
  2. Sie fordert diesen Spieler auf, eine Zahl von 1 bis 9 einzugeben. Das ist die Nummer für genau das Spielfeld, auf das der Spieler seinen Stein setzen möchte. Es sollte ein noch freies Spielfeld sein.
  3. Sie berechnet für die eingegebene Nummer die Spalte und Zeile des Feldes und
  4. fügt einen neuen Zug in die Liste der Züge ein.
  5. Sie gibt nach erfolgreichem Zug das Spielfeld mit dem neuen Zustand aus.
  6. Sie stellt fest, ob das Spiel jetzt schon zu Ende ist.
  7. Falls es noch nicht zu Ende ist beginnt sie wieder von vorn (das hört sich nach einer Schleife an, oder?)

Machen wir uns ans Werk!

Den Spieler auswählen, der anfängt


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
end

Das ist einfach. Wir legen fest, der Spieler O fängt immer an. Welcher Spieler gerade dran ist merken wir uns in der Variablen wer. Dort speichern wir den Index in der Spielerliste für den aktuellen Spieler. Der Spieler O ist der erste Spieler in der Liste, also hat wer am Anfang den Wert 0 (Null).

Der Spieler soll die Nummer eines Feldes eingeben


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  while true
    out.print "#{spieler[wer][1]} ist am Zug: " 
    nummer = ein.gets.to_i
    break if nummer == 0
  end
end

Natürlich brauchen wir eine Schleife. Wir nehmen die While-Schleife und machen sie zu einer Endlosschleife, indem wir die Schleifenbedingung auf true setzen. Das ist immer wahr, also hört die Schleife nie auf. Damit sie aber aufhört, wenn das Spiel zu Ende ist, oder die Spieler keine Lust mehr haben, bauen wir innerhalb der Schleife ein break zum Abbrechen ein.

In der Schleife fragen wir als erstes den aktuellen Spieler nach der Nummer für seinen nächsten Zug. Dann lesen wir von der Eingabe mit ein.gets die Nummer zunächst als String und verwandeln sie sogleich in eine natürliche Zahl mit dem Befehl to_i und speichern diese Zahl in der Variablen nummer.

Wird die Nummer gleich 0, dann brechen wir die Schleife ab. Null kann die Nummer genau dann werden, wenn der Spieler die Null eingibt, oder er gibt gar keine Zahl ein, sondern Buchstaben oder andere Zeichen. Dann kann der Befehl to_i aus der Eingabe keine natürliche Zahl erzeugen und gibt immer die Zahl Null zurück.

Bestimmen von Spalte und Zeile für die Nummer des Feldes

Ein Zug besteht aus der Angabe des Spielers und der Spalte und Zeile des Feldes, in das der Spieler seinen Stein setzen möchte. Die Nummer des Feldes haben wir nun vom Spieler erfragt. Wie lautet nun aber die Spalte und Zeile zu dieser Nummer? Wir brauchen eine weitere Methode, die diese Berechnung für uns ausführt. Schauen wir sie uns zunächst an und besprechen sie dann später.


# 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(nummer)
  spalte = (nummer - 1) % 3
  zeile  = ((nummer + 2 ) / 3 ) - 1
  # spalte und zeile beginnen aber bei 1, also 1 dazu addieren
  [spalte+1, zeile+1]
end

def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  while true
    out.print "#{spieler[wer][1]} ist am Zug: " 
    nummer = ein.gets.to_i
    break if nummer == 0
    spalte, zeile = nummer_in_spalte_zeile(nummer)
  end
end

Die Methode nummer_in_spalte_zeile liefert uns für eine übergebene Nummer eine Liste mit zwei Zahlen: die Spalte und die Zeile an der sich das Feld auf dem Spielfeld mit der Nummer befindet.

Angenommen, Spalte und Zeile würden wir mit 0, 1, 2 abzählen. Dann berechnet sich die Spalte mit der Formel (nummer - 1) % 3. Das Feld mit der Nummer 6 hat also als Spalte die (6 - 1) % 3 = 5 % 3 = 2, also die dritte Spalte. Den Operator % (der modulo Operator oder Rest-bei-Division Operator) hast du schon kennen gelernt. Er bedeutet: Gib mir den Rest bei der Division! % 3 bedeutet somit: der Rest bei der Division durch 3. Und der Rest ist 2, wenn man die 5 durch 3 teilt. Und 2 ist der Index der dritten Spalte, wenn wir mit 0, 1, 2 zählen.

Die Berechnung der Zeile ist etwas komplizierter, aber durch etwas herumprobieren leicht aus der Tabelle im Kommentar zur Methode nummer_in_spalte_zeile abzulesen. zeile = ((nummer + 2 ) / 3 ) - 1. Hier must du nur bedenken, dass Ruby beim Dividieren mit ganzen Zahlen immer auf die nächstgelegene ganze Zahl abrundet. Für Ruby ist also 8 / 3 gleich 2. Es ist also das Ergebnis der ganzzahligen Division (mit Rest).

Bevor die Methode die berechneten Werte für Spalte und Zeile zurückgibt, muss natürlich noch eine 1 addiert werden, weil wir die Spalten und Zeilen ja doch mit 1, 2, 3 abzählen wollen.

Einfügen des neuen Zuges

Jetzt kennen wir die Spalte und Zeile, die der aktuelle Spieler besetzen will. Wir können nun den neuen Zug in die Zugliste einfügen.


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  while true
    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_hinzu(spieler[wer][0], spalte, zeile, zuege)
  end
end

Aber Moment mal! Was ist, wenn der Spieler eine Nummer für ein Feld eingibt, das bereits von seinem Gegenspieler besetzt wurde? Das dürfen wir nicht zulassen. Wir müssen die Methode zug_hinzu aus der letzten Lektion abändern:


# 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
  # ... aber nur, wenn der Zug erlaubt ist!
  zuege << [wer, spalte, zeile] if erlaubt
  erlaubt
end

Die Methode zug_okay liefert nun true zurück, wenn der Zug erlaubt ist und gemacht wurde, andernfalls liefert sie false zurück. Das können wir benutzen, um den Spieler erneut um die Eingabe einer Nummer zu bitten. Wir ändern also unsere bisherige Methode play_2_spieler wie folgt ab:


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  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
  end
end

Wir merken uns in der Variablen zug_okay, ob der Zug gemacht werden konnte oder nicht. Solange diese Variable den Wert false hat, wiederholen wir die Aufforderung zur Eingabe eines Zuges. Dafür nehmen wir eine neue Schleife, die Until-Schleife. Sie führt einen Codeblock solange aus, bis die Bedingung wahr wird (until, engl. solange bis) .

Den ersten Wert von zug_okay setzen wir auf false, somit läuft die Until-Schleife zumindest einmal durch.

Den Rückgabewert von zug_hinzu nehmen wir als neuen Wert für zug_okay. Hat der Spieler sich nicht vertippt und eine gültige Zahl eingegeben, dann wurde der Zug hinzugefügt und die Variable zug_okay hat den Wert true. Dann ist die Until-Schleife beendet.

Spielfeld nach dem Zug ausgeben

Das ist einfach, dafür haben wir ja schon eine Methode.


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  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)
  end
end

Feststellen, ob das Spiel schon zu Ende ist


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  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)
    break if ist_beendet?(zuege) or !zug_okay
  end
end

Wir verlassen die While-Schleife, wenn das Spiel beendet ist, oder der Spieler bewusst eine 0 (Null) oder einen Buchstaben eingegeben hatte, um das Spiel zu beenden. Dann nämlich hatte die Until-Schleife keine Chance, die Variable zug_okay auf true zu setzen, weil zuvor das break die Until-Schleife unterbrochen hat.

Es bleibt noch die Methode ist_beendet? zu implementieren.


def ist_beendet?(zuege)
  zuege.size >= 9
end

Das Spiel ist aus, wenn 9 oder mehr Züge (mehr als 9 können eigentlich nicht vorkommen, wenn zug_hinzu richtig arbeitet) gemacht wurden. Wir schicken dazu die Nachricht size (engl. Größe) an die Zugliste und erhalten damit die Anzahl der Elemente in der Zugliste zurück.

Es könnte aber passieren, dass wir aus Versehen eine ungültige Zugliste übergeben, eine die es gar nicht gibt. Wir testen also lieber vorher, ob die Variable zuege nicht den Wert nil hat. Dazu verwenden wir den ternären Operator, den du auch schon kennst, weil man so den Test in einer Zeile unterbringt:


def ist_beendet?(zuege)
  zuege == nil ? false : zuege.size >= 9
end

Nächster Spieler ist dran


def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  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)
    break if ist_beendet?(zuege) or !zug_okay
    wer += 1
    wer %= 2
  end
  spieler[wer]
end

Wir erhöhen den Index für die Spielerliste um 1. Es gibt aber nur den Index 0 und den Index 1. Wenn wir schon bei 1 sind und noch eines dazu addieren wären wir bei 2. Somit machen wir anschließend noch einmal eine Division durch 2 mit Rest und nehmen den Rest als neuen Wert für unseren Index. So können wir sicherstellen, dass der Index immer abwechselnd 0 oder 1 ist.

Irgendwann erfolgt durch den Aufruf von ist_beendet? ein break in der While-Schleife, spätestens nach 9 gültigen Zügen. Dann geben wir als letztes in unserer Methode das Symbol des aktuellen Spielers zurück. Das brauchen wir dann in den nächsten Lektionen, um erkennen zu können, wer den letzten Zug gemacht hat und evtl. der Sieger ist.

Zum Schluß hier alle Methoden, die neuen und die, die wir geändert haben:


# 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

# Berechnete die spalte und zeile für die Nummer eines Feldes
def nummer_in_spalte_zeile(num)
  spalte = ((num-1) % 3)
  zeile = (((num + 2 ) / 3 ) - 1)
  [spalte+1, zeile+1]
end

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

# Lässt 2 Spieler miteinander spielen
def play_2_spieler(out, ein, zuege)
  spieler = [[:o, 'O'], [:x, 'X']]
  wer = 0
  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)
    break if ist_beendet?(zuege) or !zug_okay
    wer += 1
    wer %= 2
  end
  spieler[wer]
end

Peter und Livia

Peter: Das Spiel ist doch nicht erst beendet, wenn 9 gültige Züge gemacht wurden! So wie das Programm bisher läuft, kann man noch weiterspielen, obwohl schon einer der Spieler 3 Steine in einer Reihe hat.

Livia: Das stimmt. Das Rubyprogramm kann noch nicht erkennen, wann ein Spieler gewonnen hat. Das könntest du noch in die Methode ist_beendet? einbauen. Warte bis zur nächsten Lektion.

Peter: Die Methode ist_beendet? hat ein Fragezeichen im Namen. Was bedeutet das?

Livia: Alle Methoden in Ruby, die wahr oder falsch (true oder false) zurückgeben, sollte man durch dieses an den Namen angehängte Fragezeichen von außen schon als solche Methode erkennbar machen. Es zwingt dich aber niemand dazu. Nur ist es beim Lesen des Codes schöner.

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.

Lektion 10 - Entwicklungsumgebung 2

Erstellt von Frithjof Sat, 09 Jun 2007 20:55:00 GMT

- Änderungen
22.09.2007, rubykids.rb ist überarbeitet und erfordert nun den K-Parameter nicht mehr. Im folgenden können somit die Schritte zur Anpassung des Parameters beim Ausführen der Rubyprogramme in den verschiedenen Entwicklungsumgebungen ignoriert werden.
-

Bevor wir uns ab Lektion 11 im Rahmen eines neuen Projektes (keine Häuser mehr—versprochen!) weiter mit Ruby selbst beschäftigen diskutieren wir in dieser Lektion ein wenig über Entwicklungsumgebungen. Wir reden über
  1. Vi/Vim, der Texteditor, den wir bisher verwendet haben. Auf den ersten Blick ist es nur ein Texteditor. Aber man kann auch ohne Vim zu verlassen direkt in Vim das aktuell bearbeitete Rubyprogramm ausführen lassen. Das ist aber aus meiner Sicht umständlicher, als sich parallel ein DOS-Fenster aufzumachen.
  2. SciTE, ein Texteditor, den wir mit Ruby zusammen installiert haben. Hier ist das Ausführen von Rubycode schon besser in den Editor integriert.
  3. Eclipse, das geht nun leider nicht mehr in einem einzigen Wort zu erklären. Eigentlich kann Eclipse alles – okay fast alles. Eclipse ist keine Entwicklungsumgebung und schon gar nicht ein Texteditor, sondern eher eine Plattform-Software, die man für viele Einsatzbereiche erweitern und anpassen kann. Bekannt wurde Eclipse trotzdem hauptsächlich als Entwicklungsumgebung für die Programmiersprache Java. Eclipse macht es aber auch möglich, es zu einer Entwicklungsumgebung für Ruby auszubauen. Ich werde in den nächsten Lektionen vorwiegend nur noch mit Eclipse arbeiten, wobei ich selbstverständlich das VIM Plugin für Eclipse installiert habe :-).

Zur Entwicklungsumgebung zählt als wichtigstes Element natürlich der Texteditor, mit dem wir unseren Programmcode entwickeln. Du kannst Rubycode übrigens mit jedem beliebigen Editor anlegen, der fähig ist Textdateien zu speichern. Aber der Editor ist nicht alles. Du hast in den letzten Lektionen eine Reihe von Dateien angelegt (lektion1.rb, lektion2.rb,...), die sicher alle im Verzeichnis c:\entwicklung liegen. Je mehr du programmierst, wirst du sicher feststellen, dass es zunehmend unübersichtlich wird, in einer immer länger werdenden Liste von Dateien die passende zu suchen. Man müsste das etwas organisieren können. Zum Beispiel durch das Anlegen von Unterverzeichnissen. Womit wir schon beim nächsten Problem sind. Wir wechseln ständig zwischen dem Texteditor und dem DOS-Fenster oder dem Dateiexplorer hin und her.

Aber auch der Programmcode in der Datei selber soll organisiert sein. Er soll gut lesbar sein. Daher rücken wir ja auch bestimmte Zeilen am Anfang etwas ein. Welches der beiden folgenden Codebeispiele findest du leichter zu lesen?

require 'rubykids'

zahl = 0
3.mal do
zahl += 5
schreibe zahl
end

require 'rubykids'

zahl = 0
3.mal do
  zahl += 5
  schreibe zahl
end

Wenn wir unsere Ideen in Ruby ausprobieren möchten, erzeugen wir meistens Code, der nicht lange Bestand hat. Wir verändern ihn, löschen was raus, fügen was ein, schreiben etwas um. Dabei gibt es manchmal Zeilen, die wir nur vorübergehend deaktivieren, weil wir die Idee vielleicht später weiterführen möchten. Am einfachsten ist es dabei, wenn wir die Zeile als Kommentar mit dem # Symbol markieren. Für eine Zeile ist das in Ordnung, aber wenn wir gleich 10 oder noch mehr Zeilen auf einmal auskommentieren möchten, ist es schon mühsam in jeder Zeile das # Symbol einzufügen. Gut, wenn das unsere Entwicklungsumgebung auch könnte.

Eine Entwicklungsumgebung die ihren Namen verdient sollte uns also mindestens mit folgenden Möglichkeiten verwöhnen:
  • Sie soll Textdateien erstellen können, in die wir unseren Programmcode ablegen.
  • Sie lässt uns die Dateien in Verzeichnissen organisieren.
  • Sie führt unser Programm aus, ohne dass wir ein DOS-Fenster aufmachen müssen. Entweder stellt sie ein eigenes Fenster dafür zur Verfügung, oder sie macht das DOS-Fenster für uns auf.
  • Sie hilft uns beim Finden von Tippfehlern.
  • Sie hilft uns beim Formatieren des Codes, indem sie automatisch die Zeilen einrückt, die bspw. durch ein do und end eingerahmt werden.
  • Sie hilft uns dabei, Codezeilen auszukommentieren (d.h. das # Zeichen an den Anfang zu stellen), aber auch das Kommentarzeichen wieder zu entfernen.
  • Sie macht unseren Code bunt. Sie malt bspw. Schlüsselwörter und Zeichenketten mit verschiedenen Farben an. Das erleichtert uns die Lesbarkeit des Codes.

Vim

Unsere Rubyprogramme schrieben wir bisher mit Vim, sofern du dich nicht bereits von Anfang an für einen anderen Texteditor entschieden hattest. Vim scheint anfangs ziemlich umständlich. Es ist ungewöhnlich, dass es einen Eingabemodus und einen Commandmodus gibt, insbesondere verwirrt bei den ersten Schritten mit Vim, dass sich die Tasten im Commandmodus so ganz anders verhalten. Vielleicht hast du schon bemerkt, dass dann der Cursor nicht nur mit den Pfeiltasten Auf, Ab, Links und Rechts, sondern auch mit den Tasten K, J, H, L bewegt werden kann. Das ist ziemlich cool, solange man mit 10 Fingern über die Tastatur fegt. Andernfalls ist es eher hinderlich. Aber nicht zuletzt deswegen wird Vim nach wie vor geliebt. Es gibt sogar Anstrengungen, Vim in andere Texteditoren und Entwicklungsumgebungen einzubetten, d.h. Vim oder zumindest die wichtigsten seiner Eigenschaften auch dort verwenden zu können, wo man Vim eigentlich nicht verwenden kann.

(Gibt es eigentlich Vim Features schon für einen webbasierten Editor, gar ein Plugin für TinyMCE? Bin für jeden Hinweis dankbar.)

Hier möchte ich nicht weiter auf die Details von Vim eingehen. Du findest im Web ziemlich viele Tutorials und Hilfeseiten dazu. Am leichtesten fängst du aber mit dem Tutorial an, das bereits mit der Installation von Vim gekommen ist. Du findest es im Verzeichnis von Vim …/Vim/vim70/tutor. Öffne dort das deuschsprachige Tutorial in der Datei tutor.de—natürlich mit Vim.

Ruby SciTE Entwicklungsumgebung

Unsere Installation von Ruby kommt eigentlich schon mit einer Entwicklungsumgebung. Wir haben sie zunächst absichtlich umgangen, um ein besseres Gefühl für das Anlegen von Dateien zu bekommen. Du findest SciTE im Installationsverzeichnis von Ruby unter c:\sprachen\ruby\scite\SciTE.exe. SciTE ist ein Texteditor, der auf Scintilla basiert. Scintilla wiederum ist eine Software die den Umgang mit Programmiercode möglich macht und bietet genau das was wir eigentlich oben angesprochen haben.

Legen wir mal unser Programm aus Lektion 1 (okay, ich glaub’ dir ja, dass du es schon nicht mehr sehen kannst – du kannst dir ja für dieses Beispiel auch ein eigenes Programm ausdenken) mit SciTE an:

  1. Öffne SciTE durch Doppelklick auf c:\sprachen\ruby\scite\SciTE.exe

  2. Nachdem du den Code aus Lektion 1 eingegeben hast, müsste es etwas so auch bei dir aussehen:
  3. Wir speichern die Datei nun unter c:\entwicklung\lektion1_sciTE.rb ab. Gehe dazu im SciTE im Menü File auf Save as, suche das Verzeichnis c:\entwicklung und gib als Dateinamen lektion1_sciTE.rb ein und klicke auf Speichern.

    Nach dem Speichern erkennt SciTE an der Dateiendung .rb, dass die Datei Rubycode enthält und macht dann den Code bunt.
  4. Kann SciTE unser Programm auch ausführen? Versuchen wir es! Gehe im Menü auf Tools und dort auf Go oder drücke die F5 Taste.

    Es geht kurz ein DOS-Fenster auf und verschwindet aber wieder. Dann erscheint im Ausgabefenster von SciTE die Ausgabe unseres Programms. Aber, was dort steht ist nicht das, was wir erwartet haben, sondern es erscheint eine Fehlermeldung. Irgendwie meckert Ruby was von Invalid char…. Es liegt wieder einmal an unserer deutschen Sprache. Wir führen die Rubyprogramme im DOS-Fenster ja mit dem K-Parameter -Ku aus. Das müssen wir SciTE auch erst noch beibringen.
  5. Gehe dazu in SciTE wieder im Menü auf Options und klicke in der langen Liste, die sich öffnet auf Open ruby.properties.
  6. Es öffnet sich eine weitere Reiterkarte, die den Inhalt der Datei ruby.properties anzeigt. In dieser Datei können wir konfigurieren, wie SciTE mit unseren Rubyprogrammen umgeht. Dort kannst du bspw. die Farben für die Hervorhebung von Zeichenketten ändern. Wir suchen aber jetzt etwas anderes, nämlich die Stelle, an der SciTE den Rubyinterpreter mit unserem Rubycode aufruft. Bei mir findet sich die gesuchte Stelle in Zeile 104.

    Ergänzen wir hier einfach unseren -Ku Parameter, speichern die Datei ruby.properties, gehen wieder in unser Rubyprogramm und versuchen es erneut mit F5 es auszuführen. Jetzt klappt es!

Eclipse mit Rubyplugin

Oben habe ich schon einige Worte zu Eclipse verloren. Es ist keine Entwicklungsumgebung, obwohl Eclipse wahrscheinlich am meisten als solche verwendet wird, insbesondere für die Programmiersprache Java. Wir schauen uns hier kurz an, wie wir Eclipse als Entwicklungsumgebung für Ruby verwenden können. Los geht’s wie immer am Anfang mit der Installation, ein wenig Konfiguration und einem ersten Test.

Eclipse Download

  1. Du findest den Download für Eclipse unter www.eclipse.org/downloads. So wie die Seite heute aussieht, klickst du dort auf Download now: Eclipse SDK 3.2.2. Achte darauf, dass hinter dem Link Windows steht, da es Eclipse natürlich auch für andere Betriebssysteme gibt. Du musst aber bedenken, dass nichts kurzlebiger ist als Websiten. Egal, irgendwo findet sich auf jeder Seite immer ein Bereich für den Download, sofern es dort etwas zu downloaden gibt. Achte bei Eclipse nur darauf, dass du das SDK erwischst, egal ob es die Versionsnummer 3.2.2 (die ist im Moment aktuell) oder eine höhere ist.

  2. Du gelangst nun auf eine andere Seite und klickst dort auf den Link direkt hinter Download from:. Im Screenshot unten wäre das [Germany] Innoopract Informationssysteme GmbH (http). Nur wenn es dabei Probleme gibt, solltest du einen anderen Mirror für den Download verwenden (mirror zu deutsch Spiegel, das sind Server, die zwar an verschiedenen Orten in der Welt stehen, aber identische Software zum Download bereit halten).

  3. Nun startet der Download und du musst nur noch den Pfad auf deiner Festplatte auswählen, wo die Datei eclipse-SDK-3.2.2-win32.zip abgelegt werden soll. Die Datei ist übrigens recht groß, ca. 120 MB.

Eclipse Installation

Das Installieren von Eclipse beschränkt sich auf das Auspacken der Datei eclipse-SDK-3.2.2-win32.zip bspw. nach c:\sprachen oder c:\software. Wenn du eine Komprimierungssoftware für Dateien hast, sollte das Auspacken kein Problem sein. Wenn nicht, Windows XP kann sowas im Prinzip zwar auch, allerdings ist es mir nicht gelungen, Eclipse auf diese Weise auszupacken. Windows XP hat nach ca. 70 MB aufgegeben. Verwende daher lieber gleich ein ordentliches Komprimierungsprogramm. 7-Zip zum beispiel ist im Gegensatz zu WinZip kostenlos.

Eclipse starten

Gehe in den ausgepackten Ordner Eclipse. Dort findest du eine Datei mit Namen eclipse.exe. Mit Doppelklick auf diese Datei öffnet sich Eclipse. Das kann, je nachdem wie schnell dein Computer ist, ein wenig dauern, weil Eclipse schon ziemlich hungrig auf den Arbeitsspeicher und die Rechenzeit des Prozessors ist.

Als erstes fragt dich Eclipse nach dem Workspace. Das ist eine Ordner, in dem Eclipse die Dateien und Verzeichnisse für die Programme anlegen soll, die wir mit Eclipse entwickeln werden. Wähle zum Beispiel als Workspace das Verzeichnis c:\entwicklung\eclipse_workspace.

Wenn du stattdessen eine Fehlermeldung erhältst

deutet das darauf hin, dass auf deinem Computer Java nicht installiert ist. Keine Angst, wir werden nicht mit Java programmieren (obwohl das ebenfalls eine coole Sprache ist), aber Eclipse braucht nun mal Java, damit es arbeiten kann. Du findest eine aktuelle Java Version direkt beim Hersteller java.sun.com. Es reicht die JRE (Java Runtime Edition). Du kannst aber auch das JDK (Java Development Kit) nehmen:


Wenn du Java installiert hast, kehre hierher zurück und versuche es erneut mit dem Aufruf von Eclipse.

Okay, Eclipse ist nun gestartet und präsentiert sich beim ersten Mal mit dem Begrüßungsbildschirm.


Dort gehe ganz rechts außen auf das Symbol Workbench (das heißt so viel wie Werkbank, meint also den Bereich von Eclipse, wo wir werkeln werden.

In der Workbench angekommen sieht Eclipse nun etwa so aus:


Eclipse ist standardmäßig auf Java eingestellt. Deshalb siehst du auch den Perspektiven-Schalter oben rechts auf Java stehen. Hier brauchen wir einen anderen Schalter, auf dem Ruby draufsteht. Wo bekommen wir den her? Na klar, wir müssen wieder etwas installieren, aber diesmal innerhalb von Eclipse. Bevor wir das machen, noch ein paar Worte dazu, was eine Perspektive in Eclipse ist. Damit ist die gesamte Oberfläche gemeint, die du am Bildschirm von Eclipse sehen kannst: das Menü, die sichtbaren Knöpfchen (Toolbar, zu deutsch Werkzeugleiste), die zur Verfügung stehenden Teilfenster (Views, zu deutsch Sichten) usw. Wir brauchen für Ruby ein paar Werkzeuge und Ansichten, die uns die Arbeit mit Ruby erleichtern. Da nützen uns die Dinge, die speziell für Java gedacht sind, nicht viel weiter. Also dann installieren wir mal was für Ruby.

Eclipse fit für Ruby machen

Gehe im Menü von Eclipse auf Help, dann auf Software Updates und dort dann auf Find and Install.


Es öffnet sich ein Dialogfenster Install/Update, wo du den Knopf bei Search for new features to install anklickst und dann auf Next gehst.


Weiter geht es mit dem Dialog Install – Update sites to visit. Hier legst du eine neue Site an mit dem Button New Remote Site… und gibst als Name für die Site RDT (RDT steht für Ruby Development Tools) und als URL gibst du http://updatesite.rubypeople.org/release ein. Dann OK.


Zurück in dem Dialog für die Sites achtest du darauf, dass nur das Häkchen bei RDT gesetzt ist und gehst auf Finish.


Eclipse schaut nun online nach, ob es Updates für die gewünschte Software gibt. Da wir RDT ja noch nie zuvor installiert haben, sollte Eclipse auch etwas finden und sich mit dem Dialog Updates – Search Results von der Suche zurückmelden. Setze hier erneut das Häkchen bei RDT und gehe auf Next.


Eclipse landet im Dialog Install – Feature License. Wie sonst auch üblich muss man auch für die Software, die wir innerhalb von Eclipse installieren deren Softwarelizenz zustimmen, bevor man sie nutzen kann. Klicke also auf I accept the terms and the license aggreement und gehe auf Next.


Eclipse faßt dir nun die Installation nochmal auf dem Dialog Install – Installation zusammen. Gehe mit Finish weiter. Eclipse fängt an, die Software zu installieren.


Zum Schluß prüft Eclipse die Software und meldet auf dem Dialog Verification – Feature Verification, ob die Softwareprüfung in Ordnung ist. Meistens kommt hier aber eine Warnung Warning: You are about to install an unsigned feature. Das ist normal. Da wir RDT vertrauen, können wir hier gleich auf Install All gehen. Danach will Eclipse neu starten, was wir natürlich mit Yes erlauben und gespannt warten, bis Eclipse wieder da ist.


Okay, Eclipse ist wieder da und wir stellen nun die Perspektive auf Ruby um. Gehe dazu im Menü auf Window, Open Perspective und Other. Im Dialog der sich öffnet makiere den Eintrag Ruby und klicke OK. Das gleiche geht auch über die Perspektiven Knöpfchen am rechten oberen Rand vom Hauptanzeigefenster.


Beispielprogramm mit Eclipse

Nun wird es aber langsam ein bisschen Zeit, dass wir mit Eclipse auch mal etwas vernüftiges machen. Wie wäre es endlich mal mit einem Rubyprogramm in Eclipse? Danach ist die Lektion 10 aber auch wirklich zu Ende – ganz sicher!

In Eclipse brauchen wir zunächst ein Projekt, das unsere Dateien mit Rubycode verwaltet. Ein Projekt legst du am einfachsten an, indem du mit der rechten Maustaste in das linke Teilfenster Ruby Resources klickst (wir nennen es mal das Rubyfenster) und aus dem öffnenden Kontextmenü den Eintrag New und Ruby Project auswählst.

Dann gibst du dem Projekt einen beliebigen Namen, zum Beispiel Rubykids. Nach OK wird das neue Projekt im Rubyfenster angezeigt.

Jetzt geht es wie üblich: Wir legen eine Datei (innerhalb des Projektes) an, schreiben Rubycode hinein und führen das Programm aus. Also keine Zeit verlieren: rechte Maustaste auf das neue Projekt Rubykids öffnet das Kontextmenü, aus dem wir diesmal New und File auswählen. Der Datei geben wir den Namen lektion10.rb und schreiben etwas Rubycode hinein, z.b. den von Lektion 1


Vor dem Ausführen müssen wir allerdings noch die Datei rubykids.rb im Verzeichnis c:\entwicklung\eclipse_workspace\Rubykids (wenn dein Projekt so heißt wie meines) ablegen und die Projektanzeige im Rubyfenster auffrischen.

Beim Ausführen möchten wir ja die Ausgabe in Eclipse sehen. Dazu blenden wir uns das Ausgabefenster ein, falls es noch nicht vorhanden ist. Das geht über das Menü Window und Show View und dort wählen wir Console. Das Consolenfenster erscheint nun im unteren Bereich von Eclipse.


Jetzt wird es nochmal kurz spannend. Ausgeführt wird nämlich endlich mit dem Run As Befehl, entweder über den kleinen schwarzen Pfeil am Run-Knöpfchen in der Toolbar, oder wieder über die rechte Maustaste des Projektes. Wichtig: der Cursor muss dabei in der auszuführenden Datei stehen oder die Datei muss zumindest im Projekt markiert sein, damit Eclipse weiß, was wir ausführen möchten!

Na prima, eine Fehlermeldung. Wie sollte es auch anders sein.

Eclipse kann den Rubyinterpreter nicht finden! Wir müssen Eclipse sagen, wo wir Ruby installiert haben. Das machen wir im Menü von Eclipse über Window und Preferences…. Es öffnet sich der Dialog der zentrale Einstellungen von Eclipse beherbergt.

Hier klappen wir im Navigationsbaum links den Pfad Ruby – Installed Interpreters auf und legen mit dem Knopf Add auf der linken Seite des Dialogs einen Interpreter mit folgenden Werten an:


  • RubyVM type: Standard VM
  • RubyVM name: Ruby
  • RubyVM home directory: C:\sprachen\ruby (oder ein anderes Verzeichnis, wenn du Ruby woanders hin installiert hast).
  • Default VM Arguments: -Ku (diesmal geben wir den K-Parameter gleich mit an).

und aktivieren ihn schließlich mit dem Häkchen und OK.

Nun nochmals Ausführen wie oben beschrieben, und wir sehen die gewünschte Ausgabe. Geschafft!

Peter und Livia

Peter: Ich weiß überhaupt nicht mehr, was ich hier noch alles installieren und konfigurieren soll? Das ist ja voll kompliziert!

Livia: Du lernst währenddessen dafür aber auch eine Menge, nicht nur von Ruby oder Eclipse, sondern du lernst die Funktionsweise deines Computers insgesamt besser kennen. Es lohnt sich! Los, ich helfe dir! (Und sie installieren, konfigurieren und programmieren bis die Sonne hinter dem Monitor versinkt…)

Lektion 9 - Interaktives Ruby

Erstellt von Frithjof Mon, 21 May 2007 19:28:00 GMT

In den bisherigen Lektionen haben wir Rubyprogramme zuerst in eine gewöhnliche Textdatei geschrieben und anschließend mit dem Befehl:

C:\entwicklung>ruby lektion_xx.rb
ausgeführt. Manchmal möchtest du aber sicher nur schnell eine Idee ausprobieren. Da ist es etwas umständlich, erst eine Datei anzulegen. Für diesen Zweck gibt es das sogenannte Interaktive Ruby, abgekürzt IRB. Wir starten IRB in einem DOS-Fenster mit dem Befehl

C:\entwicklung>irb 

Direkt nach dem Start von IRB merken wir eigentlich nur am Prompt, dass wir nicht mehr nur im DOS-Fenster, sondern auch in der IRB Umgebung sind. Nach dem IRB-Prompt können wir nun echten Rubycode eintippen!

Nehmen wir unser erstes Beispiel aus Lektion 1. Mit IRB können wir den Code ausprobieren, ohne eine Datei anlegen zu müssen.

Mit dem Befehl exit oder quit verlassen wir irb wieder.

Es gibt sogar eine Website, auf der du online Ruby in einem IRB Fenster ausprobieren kannst. Probier es aus, gehe auf die Seite Try Ruby (deutsch: Probiere Ruby aus)!. Du kannst hier allerdings nicht unser require ‘rubykids’ und schreibe ”...” usw. verwenden, weil das Online-IRB unsere Datei rubykids.rb nicht kennt. Verwende daher beim Online-IRB für die Ausgabe von Text den Befehl puts anstatt schreibe. Etwa so:


puts "Hallo, grüß dich!" 

Nachteile von IRB

Der große Vorteil von IRB ist bereits angesprochen – man kann schnell mal etwas in Ruby ausprobieren.

Wo Licht ist gibt es üblicherweise aber auch Schatten. Ein genauso großer Nachteil ist nämlich, dass du nichts speichern kannst! IRB ist daher nicht geeignet, lange Programme zu schreiben. Man muss alles von Anfang an neu eingeben, wenn man IRB verlassen hat und neu startet.

Peter und Livia

Peter: Mich stört das Prompt von IRB. Wie kann ich das ändern?

Livia: Tippe im DOS-Fenster

C:\entwicklung>irb --help
ein und du erhälst Hilfeinformationen zu den möglichen Parametern, die du IRB beim Starten mitgeben kannst. Es gibt bspw. den Parameter —simple-prompt. Starte IRB damit wie folgt:

C:\entwicklung>irb --simple-prompt
>> require 'rubykids'
=> true
>> schreibe "5 + 12 = ", (5+12)
5 + 12 = 17
=> nil
>> quit
C:\entwicklung>
Nun besteht das Prompt lediglich aus >>.


Peter: Ich kann in IRB keine geschweiften Klammern eintippen. Woran liegt das?

Livia: Bei der Installation von Ruby solltest du im Schritt Choose Components alle Häkchen machen die du nur kannst. Die letzte Option ist European Keyboard (deutsch: europäische Tastatur). Falls du das Häkchen bei der Installation vergessen hast, gibt es zwei Möglichkeiten:
  1. Deinstalliere Ruby wieder (über die Systemsteuerung, Mama weiß da Bescheid) und installiere es erneut, nun aber mit der angehakten Option für die europäische Tastatur.
  2. Oder du legst manuell eine Umgebungsvariable mit Namen INPUTRC und dem Wert c:\sprachen\ruby\bin\inputrc.euro an (ändere den Pfad, wenn du Ruby woanders installiert hast).

- Änderungen
22.09.2007, Unicodeproblem mit Umlauten

Older posts: 1 2