Titelseite

Spiel

Inhalt:

0.Installation des Applets

I.Worum geht's hier eigentlich?

Wie funktioniert Reversi?
Kurzbeschreibung des Applets

II.Die Implementierung

public void init()
public void reInit()
public void paint(Graphics g)
private void rekAktionAusfuehren(int x, int y, int xPlus, int yPlus)
private void AktionAusfuehren(int spalte, int zeile)
private String spielEndet (int punkteGESAMT, int punkteWEISS, int punkte-SCHWARZ)
private void wechselGegner()
private void passen()
private void entfVERBOTEN()
private void computerZug()
private void priorFeldFuellen()
private void priorFeldUpdate()
public void mousePressed(MouseEvent e)
public void mouseReleased(MouseEvent e)
public void mouseClicked(MouseEvent e)
public void mouseMoved(MouseEvent e)

0.Installation des Applets

Das Applet befindet sich auf der Diskette mit der Online-Dokumentation. Einfach die HTML-Seite "reversi.html" aufrufen, dies ist die Startseite der Dokumentation. Mit einem Klick auf den Link "Spiel" gelangt man zum Applet.

Wird das Applet an einem anderen Ort installiert, muß die auf der Diskette vorgefundene Verzeichnishierarchie beibehalten werden, um volle Funktionalität zu gewährleisten.

Das Verzeichnis Bilder beinhaltet sämtliche Grafiken, die in der HTML-Dokumentation benötigt werden. Der Ordner Reversi beinhaltet die *.java - Dateien, der Ordner Felder liegt eine Ebene tiefer. Er beinhaltet die Grafiken, die das Applet verwendet.

I. Worum geht's hier eigentlich?

Wir haben versucht, das Spiel Reversi als Applet mit Mausbedienung zu implementieren. Es verfügt über einen gar nicht so dummen Computergegner, außerdem über eine Vorschau für gültige Züge seitens des menschlichen Spielers.

Wie funktioniert Reversi?

Das Spielfeld ist 8 mal 8 Felder groß, es gibt zwei Spieler, einer schwarz, einer weiß. In jeder Runde kann der weiße Spieler einen Stein setzen, danach ist der Spieler mit den schwarzen Steinen (hier der Computer) dran. Gesetzt wird immer auf ein freies Feld, das an ein vom Gegner besetztes Feld angrenzen muß. Dabei werden alle gegnerischen Steine, welche sich zwischen dem gesetztem und den nächsten eigenen Steinen befinden, umgedreht, das heißt, aus Schwarz wird Weiß, und umgekehrt. Dies geschieht in vertikale, horizontale und diagonale Richtung. Es muß jedoch pro Zug mindestens ein gegnerischer Stein umgewandelt werden. Ist dies aus der Spielkonstellation heraus nicht möglich, muß man passen.

Das Wichtigste bei Reversi sind die sogenannten sicheren Steine. Dies sind Steine, welche der Gegenspieler nicht mehr wegnehmen kann. Die ersten "sichern Steine" sind die Ecken. Sie können nicht mehr vom Gegenspieler umgedreht werden. Ausgehend von den Ecken sind alle angrenzenden Steine, die nicht mehr vom Mitspieler übersprungen werden können, sichere Steine.

Die grundlegende Strategie bei Reversi liegt nicht darin, so viele Steine wie möglich bei jedem Zug zu machen, sondern am Schluß die meisten zu haben. Das Spiel wird im allgemeinen erst in den letzten 3-5 Zügen entschieden.

Kurzbeschreibung des Applets

Die Oberfläche zeigt natürlich das Spielfeld an, außerdem im unteren Bereich den aktuellen Spielstand, das heißt, wieviel Steine jeder Spieler besitzt, außerdem wer gerade dran ist. An der rechten Seite bietet das Applet zwei Knöpfe: einen zum Neustart des Spiels und einen zum Passen. Passen ist natürlich nur im Einklang mit den oben erwähnten Voraussetzungen möglich.

Sobald der Spieler seinen Zug gemacht hat, übernimmt der Computer und zieht selbst. Dabei orientiert er sich an der "Sichere-Steine-Regel", indem er den Ecksteinen höhere Prioritäten als den anderen Steinen zuordnet. (Dazu später mehr)

II. Die Implementierung

Das Programm besteht aus 5 Klassen:

Das Interface Zug ist ein HotSpot, in den, je nach Bedarf, die Klassen ZugDurchfuehrung, -Vorschau oder -Zaehlen "eingefüllt" werden. Es verfügt nur über eine Methode, die macheZug. Die macheZug-Methode liefert einen boolean Wert zurück, der in die Variable zugGueltig eingefüllt wird. Viele Aktionen werden erst ausgelöst, wenn zuGueltig auf true gesetzt wurde.

Die Klasse ZugDurchfuehrung implementiert das Zug-Interface, um jederzeit in den HotSpot eingefüllt werden zu können. Dessen Methode erledigt hier den eigentlichen Zug.

Die Klasse ZugVorschau implementiert ebenfalls das Zug-Interface. Hier erledigt die Methode macheZug die Vorschau auf den möglichen Zug des menschlichen Spielers, immer abhängig von der Mausposition. Diese wird der Methode einfach übergeben. Außerdem existiert eine Methode holeVariable(), welche die lokalen Variablen mit den Werten aus der GCReversi.java befüllt.

Die Klasse ZugZaehlen implementiert genauso das Zug-Interface. Die macheZug Methode macht hier nur eines: sie zählt die Anzahl der eroberten Steine bei einer ZugVorschau für den Computer. Sie macht das zwar auch für den Spieler, nur der bekommt davon nichts mit, da diese Werte ohnehin nur für das Einteilen des Spielfeldes in anstrebenswerte und nicht anstrebenswerte Felder gebraucht werden. Außerdem wird die Methode der Klasse ZugZaehlen beim Passen aufgerufen, um zu ermitteln, ob noch Züge möglich sind.

In der Klasse GCReversi, die gleich näher erläutert wird, wird das Interface AktionSetzen deklariert, in welches die Instanzen der oben genannten Klassen eingefüllt werden können.

Beispielsweise:
Zug AktionSetzen = new ZugVorschau();
AktionSetzen = new ZugDurchfuehrung();

Wird später die Methode AktionSetzen.macheZug aufgerufen, wird immer die Aktion ausgeführt, die der momentan in den HotSpot geladenen Klasse entspricht.

Nun zum eigentlichen Spiel, der Klasse GCReversi:

Das Spielfeld besteht aus einem 2 dimensionalen IntegerArray. In jedem Feld stehen Werte von 0 bis 3. Entsprechend dieser Zahlen wird in der Methode paint() das entsprechende Bild ausgewählt und dargestellt. 0 steht für ein leeres Feld, 1 für Schwarz, 2 für Weiß und 3 für Verboten. Der ganze Zauber besteht nun darin, die Elemente des Arrays zu jeder Zeit richtig mit Zahlen zu befüllen.

Die Klasse Reversi besteht aus folgenden Methoden

public void init()

Diese Methode stellt die Startbedingungen her, lädt also die Grafiken in den MediaTracker, außerdem leert sie das SpielFeld-Array und das Prioritätenfeld-Array, welches der Computer für seine Züge benötigt. Des Weiteren werden die vier Steine in der Mitte gesetzt.

Der MediaTracker registriert alle Bilder, die im Spielverlauf benötigt werden, also das Spielfeld, die Spielsteine und diverse Knöpfe. Dann werden mit waitForAll() sämtliche registrierten Bilder geladen. So wird sichergestellt, daß bereits zu Spielbeginn alle Bilder geladen sind. Störendes Flackern, verursacht durch das Nachladen von Bildern, ist somit Geschichte.

public void reInit()

Diese Methode wird ausgeführt, wenn der Spieler auf den Neustart-Knopf der Spieloberfläche klickt. Dabei werden wieder alle Spielfelder geleert und dann die Ausgangsbelegung geladen.

public void paint(Graphics g)

Die paint-Methode wird automatisch von der Java Virtual Machine aufgerufen und sie setzt je nach Markierung im SpielFeld-Array die passenden Graphiken in die Oberfläche. Außerdem zeigt sie die Statistiken an, also die Anzahl der bereits eroberten Steine pro Spieler.

Zusätzlich wird die paint(Graphics g) Methode nach jeder Aktion, die Auswirkungen auf das sichtbare Spielfeld hat, aufgerufen, diese wären: mousePressed(), mouseClicked(), mouseReleased() und mouseMoved().

private void rekAktionAusfuehren(int x, int y, int xPlus, int yPlus)

In dieser Methode wird eine gerade Linie (diagonal, vertikal oder horizontal)des Gegners bis zum Ende abgewandert. Sollte sich dort ein Stein des gerade aktuellen Spielers befinden, so wird beim Zurückwandern mittels macheZug die gerade im HotSpot geladene Aktion ausgeführt. Ist jedoch das Spielfeld zu Ende oder die Rekursion trifft auf ein leeres Feld, wandert sie unverrichteter Dinge zurück.

Im Detail: Die rekursive Methode addiert auf den Punkt P(x|y) immer die Variablen xPlus bzw. yPlus auf. xPlus und yPlus ändern sich während dem Ausführen der Methode nicht, also wandert der Punkt P(x|y) immer geradeaus, pro Rekursionsaufruf um ein Feld. Die Richtung, in die er wandert, wird von xPlus und yPlus vorgegeben.

Die Rekursion ruft sich so lange selbst auf, bis sie entweder auf einen gegnerischen Stein stößt, auf ein leeres Feld oder an den Rand des Spielfeldes. Mit jedem Aufruf wandert auch der Punkt P(x|y) um ein Feld weiter. Beim Auftreffen an den Rand des Spielbrettes geschieht in der Abbruchbedingung nichts, trifft man auf einen gegnerischen Stein, so wird auf dem Weg zurück aus der Rekursion heraus immer die AktionSetzen.macheZug(...)-Methode des Interfaces ausgeführt. Das Resultat dieses Aufrufes hängt von der im Interface geladenen Klasse ab.

private void AktionAusfuehren(int spalte, int zeile)

Diese Methode überprüft, ob die gerade im Interface (HotSpot) geladene Aktion möglich ist, dann wird sie ausgeführt, in dem die rekAktionAusfuehren(...) gestartet wird.

In einer Schleife wird die Methode rekAktionAusfuehren(...) 9mal aufgerufen, einmal mit den Koordinaten des Feldes, über welchem die Methode AktionAusfuehren(...) ausgelöst wurde, und je einmal pro umliegendes Feld.

Die an die Rekursion übergebenen Parameter entsprechen den Koordinaten der umliegenden Felder relativ zum auslösenden Feld. Entsprechend hat das Feld links neben dem auslösenden Feld die Koordinaten xPlus == -1, yPlus == 0. Da xPlus und yPlus in der Rekursion immer auf die verwendeten Koordinaten aufaddiert werden, ist somit die Richtung, in der die nachfolgenden Felder abgewandert werden, festgelegt.

private String spielEndet(int punkteGESAMT, int punkteWEISS, int punkte-SCHWARZ)

Diese Methode wird von paint(Graphics g) aufgerufen und überprüft über diverse Bedingungen, ob das Spiel schon zu Ende ist. Die Methode liefert auf jeden Fall einen String zurück. Ist das SpielFeld-Array voll, wird die Farbe des Spielers, der die meisten Steine besitzt und somit Sieger ist, zurückgegeben. Bei gleicher Anzahl Steine lautet der Inhalt des Strings "Unentschieden".

Die Variable int zaehlerPass steht zu Beginn des Spiels auf 2 und wird immer dann um 1 erniedrigt, wenn ein Spieler keinen Zug mehr machen kann und passen muß. Kann der Gegenspieler dann auch nicht ziehen, wird die Variable nochmals erniedrigt und steht somit auf 0. Kann der Gegner jedoch einen Zug machen, wird zaehlerPass wieder auf 2 gesetzt. In der Methode spielEndet(...) wird dies berücksichtigt, ist zaehlerPass == 0, wird das Spiel mit "Unentschieden" beendet.

Ist das Spiel noch nicht zuende, gibt die Methode lediglich die Farbe des aktuellen Spielers aus.

private void wechselGegner()

Diese Methode sorgt für den Wechsel der Spieler. Die Variable bekommt den Integer-Wert des Gegners und umgekehrt.

private void passen()

Wie oben schon erwähnt, kann auch die Situation auftreten, daß kein Zug möglich ist. In diesem Fall muß man passen, also den Gegner spielen lassen. Diese Methode überprüft bei einem Klick auf den Passen-Knopf, ob Züge möglich sind. Ist dem nicht so, so wird der Spieler gewechselt.

Tatsächlich wird im HotSpot eine Instanz der Klasse ZugZaehlen eingesetzt, und die Methode AktionAusfuehren(...) für jedes der 64 Spielfelder aufgerufen. Die rekursive Methode kann, sobald ein Zug möglich ist, die Variable zugGueltig = true setzen. Es muß also zugGueltig = false gesetzt bleiben, damit ein Passen möglich ist.

Wenn der menschliche Spieler (immer WEISS) paßt, muß vor dem Computerzug die Farbe gewechselt werden, damit der Computer nicht mit den Steinen des Spielers weiterspielt.

private void entfVERBOTEN()

Diese Methode entfernt vorhandene Kreuze (kennzeichnen einen ungültigen Zug) sobald die Maustaste losgelassen wird. Es wird in einer Schleife das gesamte SpielFeld-Array überprüft. Befindet sich ein Feld auf dem Wert VERBOTEN (==3), wird es kurzerhand auf LEER (==0) gesetzt.

private void computerZug()

Diese Methode führt den Zug des Computers durch. Dazu wird zunächst jedes Feld mit einer Priorität versehen und entsprechend dieser wird dann gezogen. Sollte kein Zug möglich sein, paßt der Computer natürlich.

Der Computer verwendet zum Ermitteln des strategisch günstigsten Feldes die im folgenden beschriebenen Methoden priorFeldFuellen() und priorFeldUpdate(), wobei erstere letztere automatisch aufruft.

Das auf diese Weise gefundene günstigste Feld wird in einem Array namens maxPrioritaet festgehalten. Der Computer füllt den HotSpot mit einer Instanz der Klasse ZugDurchfuehrung und besetzt das Feld, das im Array angegeben wird. Ist der Wert im Array allerdings 0, bedeutet dies, daß kein besetzbares Feld gefunden wurde, der Computer muß passen() ausführen.

private void priorFeldFuellen()

Hier werden die für den Zug des Computers benötigten Prioritäten berechnet. Ein Integerarray mit den exakt selben Ausmaßen des SpielFeld-Arrays wird Feld für Feld abgewandert. Dabei bekommt jedes Feld dieses PriorFeld genannten Arrays einen Integer-Wert zugewiesen, der die Anzahl der zu erobernden Steine von diesem Feld aus repräsentiert.

Das Array maxPrioritaet kann 3 Werte aufnehmen, den Wert des PriorFeld-Arrays, eine Spalte und eine Zeile.

Wieder wird im HotSpot eine Instanz der Klasse ZugZaehlen installiert, und diese wird mit einer Schleife auf jedem Feld des SpielFeld-Arrays ausgeführt. ZugZaehlen erhöht mit jedem Aufruf eine globale Variable um 1, nach komplettem Durchlauf der Rekursion rekAktionAusfuehren(...) in eine Richtung wird das Ergebnis auf die bereits im PriorFeld-Array vorhandene Zahl aufaddiert.

Nachdem die gesamte Zahl der theoretisch zu erobernden Steine auf einem Feld ermittelt wurde, wird noch die Methode priorFeldUpdate() ausgeführt, die noch bestimmte strategische Änderungen vornimmt.

Schließlich wird überprüft, ob der ermittelte Integerwert der bisher größte Wert ist. Ist er das, wird er mitsamt der Spalte und der Zeile, in der er gefunden wurde, in das maxPrioritaet-Array gefüllt. Es ist nachvollziehbar, daß am Ende der Schleife, die alle 64 Felder überprüft, im maxPrioritaet-Array das Feld mit dem größten Wert verzeichnet ist. Und in dieses Feld setzt der Computer seinen Stein.

private void priorFeldUpdate(int spalte, int zeile)

Diese Methode setzt die Prioritäten der Eckfelder auf den höchsten Wert von 64, da diese ja sehr wichtig für einen Sieg sind, außerdem setzt sie den Wert der Felder auf 1, die direkt an die Eckfelder angrenzen, da diese ja dem menschlichen Spieler Tür und Tor zu den Ecken öffnen würden.

In einfachen If-Abfragen wird also abgefragt, ob ein Zug in den Eck-Feldern ((spalte ==1 || spalte == 8) && (zeile == 1 || zeile == 8)) möglich ist, wenn ja, werden diese mit dem Wert 64 befüllt. Analog wird mit den Feldern, die direkt an die Ecken anschließen, verfahren.

Zu guter Letzt müssen noch die Mausbewegungen und -klicks abgefragt werden. DA die EventHandler-Interfaces verlangen, daß alle darin angegebenen Methoden implementiert werden, wir in unserem Applet jedoch nicht alle davon benötigen, werden nachfolgend nur die erklärt, die tatsächlich eine Funktion ausüben:

public void mousePressed(MouseEvent e)

Hier wird die Mausposition festgestellt, und dann, sollte das Spielfeld leer sein, der Zug durchgeführt (Im Detail: Der HotSpot wird mit einer Instanz der Klasse ZugDurchfuehrung versehen und die Methode AktionAusfuehren(...) aufgerufen). Ist dies gut gegangen, so wird an den Gegner übergeben.

Die Überprüfung, ob sich die Maus zum Zeitpunkt des Drückens im Tatsächlichen Spielfeld (Spalte und Zeile größer 0 und kleiner 9) befindet, dient lediglich dazu, ArrayIndexOutOfBoundsExceptions vorzubeugen.

public void mouseReleased(MouseEvent e)

Hier wird die Methode entfVERBOTEN() aufgerufen, es werden alle Kreuzchen auf dem Spielfeld gelöscht.

public void mouseClicked(MouseEvent e)

Hier werden die Mauspositionen wieder abgefragt und bei passenden Koordinaten das Spiel neu gestartet oder gepaßt.

public void mouseMoved(MouseEvent e)

Hier wird auch mal wieder die Mausposition abgefragt und dann der mögliche Zug vorgeführt. (Im Detail: Der bereits zur Genüge bekannte HotSpot wird mit der Instanz der Klasse ZugVorschau versehen und die Methode AktionAusfuehren(...) aufgerufen.)

Die Überprüfung, ob sich die Maus zum aktuellen Zeitpunkt im Tatsächlichen Spielfeld (Spalte und Zeile größer 0 und kleiner 9) befindet, dient lediglich dazu, ArrayIndexOutOfBoundsExceptions vorzubeugen.


Back To Top