![]() |
|
Zur praktischen Anwendung der Kenntnisse, die in diesem Kapitel vermittelt wurden, wird in diesem Teil des Kapitels erläutert, welche Logik der Server des Spiels „Schiffe versenken" hat. Im Einzelnen sind dies die folgenden Aufgaben:
Im Folgenden werden nach der Erklärung der Klassenhierarchie diese Aufgaben einzeln vorgestellt. Es sei nochmals darauf hingewiesen, dass insbesondere die Spiellogik Bestandteil aller folgenden Anwendungsbeispiele sein wird. Abb. 3.15: Klassenhierarchie des Beispiels Zur Realisierung der Spiellogik wird die in Abb. 3-15 angegebene Klassenhierarchie verwendet. Hierbei sind Klassenbeziehungen mit durchgezogenen Linien gekennzeichnet, während Aufrufe durch gestrichelte Linien dargestellt sind. Aufgabe des Hauptprogramms ist die Steuerung der Server-Anwendung. Zur Adressierung der Spielfunktionen wird die Klasse ServerSpiel angesprochen, die anschließend die Schiffe des Servers platziert (Klasse Schiffe) bzw. selbst schießt oder Schüsse des Clients auswertet (Klasse ServerSpiel). Die Eigenschaften des Spielfelds erben die Klassen Schiffe bzw. ServerSpiel hierbei von der Klasse Spielfeld. Hauptprogramm Aufgabe des Hauptprogramms, das in der Klasse Server implementiert ist, ist die Steuerung der gesamten Server-Anwendung. Diese soll im folgenden Schritt für Schritt durchgegangen werden. Setzt man die Code-Stücke wieder zusammen, so erhält man das übersetzungsfähige Programm.
import java.awt.*; static ServerSpiel ss; while(true) { // Angenommen, da waere ein Client, der die Verbindung aufbaut } } Zunächst werden die Komponenten des Packages java.awt importiert, zu denen auch die Klasse Point gehört, die wiederum zur Verwaltung von Koordinaten benötigt wird. Anschließend wird geprüft, ob ein Client eine Verbindung aufbaut. Ist dies der Fall, so wird ein Server-Spiel initialisiert und die Methode spiele aufgerufen.
static void spiele () { boolean serverDran=false, spielEnde=false, einTreffer=false,zweiTreffer=false; Im Anschluss wird die Methode spiele implementiert, in der zunächst wichtige Variablen definiert werden. Im nun folgenden Spielablauf wird zunächst festgelegt, wie im Falle eines Schusses des Clients zu verfahren ist.
while (spielEnde==false) { if (serverDran == false) { //Warten, bis der Client eine Position (x,y) schickt Zuerst wird eine Schleife betreten, die solange durchlaufen wird, bis das Spiel beendet ist. Anschließend wird auf den ersten Schuss des Clients gewartet. Aus Gründen der Höflichkeit darf in diesem Fall der Spieler immer beginnen. In der nun folgenden Bedingung wird festgestellt, ob der Server getroffen wurde und der Spieler somit nochmals schießen darf.
if (ss.rechnerTreffer(new Point(x,y))) { ss.schiffZahlServer--; if (ss.schiffZahlServer==0) { //Wir haben verloren???? } } else serverDran = true; } Hat der Spieler getroffen, so wird die Anzahl der Schiffe des Servers um eins verringert. Weiterhin wird die Variable serverDran auf false gesetzt und damit gewährleistet, dass der Server nicht an der Reihe ist. Ist die Zahl der Schiffe des Servers gleich null, so wird das Spiel beendet und der Spieler hat gewonnen.
else { while (serverDran){ //Jetzt schiessen wir koord=ss.schiesseInsBlaue(); } koord=ss.sucheZweitenTreffer(); } //Abschuss einTreffer=zweiTreffer=false; } else { //frage Client, ob Treffer } } //gewonnen } } } }}} Ist der Server an der Reihe, so wird zunächst ins Blaue geschossen. Ist dieser Schuss erfolgreich, so wird der zweite Schuss auf ein Feld abgegeben, das dem Trefferfeld benachbart ist. Anderenfalls ist der Spieler wieder an der Reihe. Zur Absicherung wird hier bereits geprüft, ob Koordinaten zurückgegeben werden. Ist auch der zweite Schuss erfolgreich, so ist die Richtung des Schiffs bekannt, weshalb die Funktion sucheZweitenTreffer mit den Parametern der ersten beiden Treffer aufzurufen ist. Anschließend wird mittels der Methode schiffVersenken versucht, die verbleibenden Felder des zu lokalisierenden Schiffs zu finden. Wenn hierbei eine null zurückgegeben wird, ist das Schiff versenkt. Zu Ende der Methode wird überprüft, ob noch Schiffe vorhanden sind, die versenkt werden müssen. Klasse Spielfeld In der Klasse Spielfeld werden zunächst wichtige Konstanten definiert, die bspw. den Zustand eines Spielfelds angeben. Weiterhin werden die Objekte für die Schiffe und für das gesamte Spielfeld deklariert. Im Konstruktor wird das Spielfeld-Objekt initialisiert.
import java.awt.*; public final int WASSER = 0; spiel = new int[10][10]; for (int j=0;j<10;j++) spiel[i][j]=FREI; } } Klasse Schiffe In der Klasse Schiffe, die eine Subklasse der Klasse Spielfeld darstellt, werden die Schiffe des Servers deklariert und initialisiert. Hierzu sind geeignete Koordinaten zu finden, die in Einklang mit den Spielregeln stehen. Zunächst werden die Klassendefinition und der Konstruktor vorgestellt.
import java.awt.*; Spielfeld sfeld; this.sfeld = spielfeld; } Point[] s = new Point[2]; } for (int i=0; i<10;i++) for (int j=0;j<10;j++) if (sfeld.spiel[i][j]!=SCHIFF) sfeld.spiel[i][j]=FREI; } Nach der Initialisierung der Schiffe muss deren Position bestimmt werden. Die folgende Methode wird daher aus dem Hauptprogramm aufgerufen, um alle Schiffe in einem Zug anzulegen.
public void stelleAlleSchiffe() { stelleSchiff(5, fuenfer); } In der nun folgenden Methode wird jedes Schiff einzeln angelegt. Hierzu wird zuerst die Richtung zufällig festgelegt und anschließend geprüft, welche Koordinaten für ein Schiff zur Verfügung stehen. Es ist zu beachten, dass beim Stellen der Schiffe die Spielregeln eingehalten werden müssen.
private void stelleSchiff(int laenge, Point[] s){ boolean isHorizontal, gesetzt=false; //Zuerst Ausrichtung des Schiffs festlegen. Ist ein //Linke Ecke des Schiffs ecke.x = (int)(Math.random()*(10-laenge)-1); } else { ecke.y = (int)(Math.random()*(10-laenge)-1); } ecke.x=0; if (ecke.y<0) ecke.y=0; if (schiffPosition(ecke, isHorizontal, laenge)){ gesetzt = true; } } } Die Schleife wird in der Methode solange durchlaufen, bis eine korrekte Position ermittelt wurde. Dies wird durch die Methode schiffPosition ermittelt. Wurde eine korrekte Position gefunden, so werden in der Methode setzeSchiff die Koordinaten des Schiffs auf den Wert BELEGT gesetzt. Es sollte hierbei abgeprüft werden, ob ein Zufallswert von null erzeugt wurde, da sich in diesem Fall negative Koordinatenwerte ergeben. Die folgende Methode überprüft, ob die Spielfelder frei sind und daher belegt werden dürfen.
private boolean schiffPosition(Point p, boolean richtung, int laenge) { boolean test = true; if ((richtung==true)&&((p.x+i)<10)) //horizontal test=(test&&(sfeld.spiel[p.x+i][p.y]==FREI) ); else if ((richtung==false)&&((p.y+i)<10)) test=(test&&(sfeld.spiel[p.x][p.y+i]==FREI) ); } } Wurde eine freie Position gefunden, so kann ein Schiff dort positioniert werden.
private void setzeSchiff(Point[] s, Point p, boolean richtung, int laenge){ int i, j=0; s[0].x = p.x; } else { s[0].x = p.x; } Anschließend müssen die Felder des Schiffs als belegt markiert werden, aber auch alle Felder rings um das Schiff, da nach den Spielregeln dort kein weiteres Schiff angebracht werden darf. Schiffe werden nur in Feldern platziert, die als frei markiert sind.
//Felder des Schiffs als belegt markieren if (richtung==true) //horizontal sfeld.spiel[p.x+i][p.y]=SCHIFF; else sfeld.spiel[p.x][p.y+i]=SCHIFF; //Felder um das Schiff als besetzt markieren for (i = p.x-1; i < p.x+laenge+1; i++) for (j =p.y-1; j < p.y+2; j++){ if ((i<10)&&(j<10)&&(i>=0)&&(j>=0)) if (sfeld.spiel[i][j] ==FREI) sfeld.spiel[i][j] = BELEGT; } } else { for (i = p.x-1; i < p.x+2; i++) for (j =p.y-1; j < p.y+laenge+1; j++) { if ((i<10)&&(j<10)&&(i>=0)&&(j>=0)) if (sfeld.spiel[i][j] ==FREI) sfeld.spiel[i][j] = BELEGT; } } } Klasse ServerSpiel Aufgabe der Klasse ServerSpiel ist die Verwaltung der Spielfelder, der Schiffe und die Auswertung von Trefferinformationen. Zunächst wird die Klasse definiert, wobei das Objekt reSchiff, das die Schiffe des Servers verwaltet, sowie das Objekt schuss, das Schussinformationen verwaltet, deklariert werden. Weiterhin werden die Spielfelder, die Anzahl der Schiffe von Server und Spieler und drei Koordinaten definiert. Während die ersten beiden Koordinaten benötigt werden, um die Richtung eines getroffenen Schiffs festzuhalten, dient die dritte dazu, die noch nicht gefundenen Felder eines Schiffs des Spielers zu treffen.
import java.awt.*; Schiffe reSchiff; Im folgenden Konstruktoraufruf werden die Spielfelder initialisiert und die Schiffe des Servers aufgestellt.
public ServerSpiel() { //Leere Spielfelder anlegen } Im Anschluss folgen die Methoden, mit denen der Server Koordinaten ermittelt, an denen Schiffe des Clients sein können. Zunächst wird auf ein zufällig gewähltes Feld geschossen. Da sich als Zufallszahl auch eine Null ergeben kann, muss sichergestellt werden, dass in diesem Fall keine negativen Werte errechnet werden.
public Point schiesseInsBlaue () { koordinate1 = new Point(); koordinate1.x = (int)(Math.random()*10-1); koordinate1.x=0; if (koordinate1.y<0) koordinate1.y=0; if (koordinate1.x>9) koordinate1.x=9; if (koordinate1.y>9) koordinate1.y=9; } while (spieler.spiel[koordinate1.x][koordinate1.y]!=FREI); } Meldet der Client einen Treffer, so muss zuerst das hierzugehörige Feld im Spielfeld-Objekt als Treffer markiert werden. Anschließend werden um dieses Feld weitere Treffer gesucht. Im Anschluss daran ist die Methode abs definiert, die den Betrag einer Zahl errechnet.
public Point sucheZweitenTreffer () { int i, j; spieler.spiel[koordinate2.x][koordinate2.y]=spie ler.WASSER; koordinate2=null; for (j=-1; j<2;j++) if ((abs(i)!=abs(j))) if ((koordinate1.x+i>=0)&&(koordinate1.x<1 0)) if ((koordinate1.y+j>=0)&&(koordinate 1.y<10)) if(spieler.spiel[koordinate1.x +i][koordinate1.y+j] ==FREI){ koordinate2=new Point(); } return koordinate2; } if (i >=0) return i; else return -i; } Sind zwei Treffer erzielt, so ist die Richtung des Schiffs bekannt. Die folgenden Schüsse müssen horizontal oder vertikal abgefeuert werden, bis der Client mitteilt, dass das Schiff versenkt ist.
public Point schiffVersenken () { boolean isHorizontal=false; spieler.spiel[koordinate3.x][koordinate3.y]=spie ler.SCHIFF; isHorizontal=true; Der nun folgende Teil weist eine erhebliche Komplexität auf. Zuerst wird je nach Ausrichtung des Schiffs nach weiteren Treffern rechts bzw. unten gesucht. Hierbei kann entweder ein neuer Treffer gefunden werden oder ein Wasserfeld. Das darauf folgende Segment erfüllt die gleiche Aufgabe für die Suche nach links bzw. oben.
//Zuerst Suche der weiteren Koordinaten nach rechts bzw. nach unten while (i<10) { i++; if (koordinate1.x+i<10) { if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==FREI){ koordinate3.x=koordinate1.x+i; } else if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==WASSER) //Ende des Schiffs erreicht, breche diese Schleife ab } } else { // Vertikale Richtung if (spieler.spiel[koordinate1.x][koordinate1.y +i]==FREI){ koordinate3.x=koordinate1.x; } else if (spieler.spiel[koordinate1.x][koordinate1.y +i]==WASSER) //Ende des Schiffs erreicht, breche diese Schleife ab } } } i--; if (koordinate1.x+i>=0){ if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==FREI){ koordinate3.x=koordinate1.x+i; } else if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==WASSER) i=-11; } } else { if (koordinate1.y+i>=0){ if (spieler.spiel[koordinate1.x][koordinate1.y+i]==FREI ){ koordinate3.x=koordinate1.x; koordinate3.y=koordinate1.y+i; return koordinate3; } else if (spieler.spiel[koordinate1.x][koordinate1.y+i]==WASS ER) i=-11; } } } //Felder um versenktes Schiff als besetzt markieren for (i = 0; i < 10; i++) for (int j =0; j < 10; j++) if (SchiffInDerNaehe(i,j)) spieler.spiel[i][j]=BELEGT; return koordinate3; } //Befindet sich rund um die jetzige Position ein anderes Schiff? if (spieler.spiel[x][y]==SCHIFF) return false; if ((x-1)>=0){ if ((y-1) >=0) if (spieler.spiel[x-1][y-1]==SCHIFF) return true; if (spieler.spiel[x-1][y]==SCHIFF) return true; if ((y+1) <10) if (spieler.spiel[x-1][y+1]==SCHIFF) return true; } if ((y-1) >=0) if (spieler.spiel[x][y-1]==SCHIFF) return true; if ((y+1) <10) if (spieler.spiel[x][y+1]==SCHIFF) return true; if ((x+1) <10){ if ((y-1) >=0) if (spieler.spiel[x+1][y-1]==SCHIFF) return true; if (spieler.spiel[x+1][y]==SCHIFF) return true; if ((y+1) <10) if (spieler.spiel[x+1][y+1]==SCHIFF) return true; } } Die nun folgenden Methoden rechnerTreffer, stelleTrefferFest bzw. treffer werten aus, ob der Client die Schiffe des Servers getroffen hat. Hierzu ist die folgende Steuermethode notwendig, die die Koordinaten des Clients als Parameter akzeptiert:
public boolean rechnerTreffer(Point p) { //sind wir selber getroffen? return true; else return false; } In der folgenden Routine wird ausgewertet, ob eines der Schiffe des Servers getroffen wurde. Hierbei wird Schiff für Schiff überprüft, ob die übergebenen Koordinaten des Clients entweder zwischen der horizontalen Anfangs- und Endposition eines der Schiffe liegen oder entsprechend zwischen der vertikalen Anfangs- und Endposition.
public boolean stelleTrefferFest(Point p) { boolean resultat = false; } public boolean treffer(int ind, Point p, Point[] s) { boolean resultat = false; if (rechner.spiel[p.x][p.y]==SCHIFF){ rechner.spiel[p.x][p.y]=VERSENKT; } if (rechner.spiel[p.x][p.y]==SCHIFF){ rechner.spiel[p.x][p.y]=VERSENKT; } } } Da der Client erfahren muss, wann ein Schiff des Servers zerstört ist, muss neben dem allgemeinen Zähler auch für jedes Schiff festgehalten werden, wie viele Felder noch nicht getroffen wurden. Nach Ende der letzten Methode darf nicht vergessen werden, die Klassendefinition durch eine geschweifte Klammer zu beenden. Ausführung der Application Nach Übersetzung der Klassen kann das Programm mittels der Anweisung java Server ausgeführt werden. Das vollständige Spiel „Schiffe versenken" liegt allerdings erst dann vor, wenn auch die Client-Komponenten bzw. die Netzwerkfunktionalität implementiert sind. Das vollständige Spiel ist in Kapitel 7 beschrieben.
|
|
|