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: Automatisches Aufstellen der Schiffe des Servers. Hierbei gilt die Regel, dass ein Schiff aus zwei Elementen, zwei aus drei Elementen, eines aus vier Elementen und ein Schiff, das aus fünf Elementen besteht, aufgestellt werden können. Schiffe dürfen nur horizontal oder vertikal platziert werden und dürfen sich weiterhin nicht berühren. Schießen auf Schiffe des Clients nach der Regel, dass jeweils nach einem Treffer ein weiterer Schuss abgegeben werden darf. Reagieren auf Schüsse des Clients. 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 Klassenhierarchie 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.*; public class Server { static ServerSpiel ss; public static void main(String[] arguments) { while(true) { // Angenommen, da waere ein Client, der die Verbindung aufbaut //Initialisieren der Server-Komponente ss = new ServerSpiel(); spiele(); } } 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; Point koord = new Point(0,0); int x=0,y=0; 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--; //Wir sind getroffen, teile dies dem Client mit serverDran=false; if (ss.schiffZahlServer==0) { //Wir haben verloren???? spielEnde=true; return; } } 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 if (einTreffer==false){ koord=ss.schiesseInsBlaue(); einTreffer=true; //Frage den Client ob Treffer. //wenn nicht: einTreffer=false, serverDran=false //ss.spieler.spiel[koord.x][koord.y]= // ss.spieler.WASSER; //falls Treffer: ss.schiffZahlClient--; } if ((zweiTreffer==false)&& (einTreffer==true)) { koord=ss.sucheZweitenTreffer(); zweiTreffer=true; //Frage den Client ob Treffer. //if Treffer beim Client: Richtung des Schiffes jetzt //bekannt. //anderenfalls: zweiTreffer=false, ServerDran = false //ss.spieler.spiel[koord.x][koord.y]= // ss.spieler.WASSER; //sonst: ss.schiffZahlClient--; } if ((zweiTreffer==true)&& (einTreffer==true)) { //Abschuss koord=ss.schiffVersenken(); if (koord==null) {//versenkt einTreffer=zweiTreffer=false; } else { //frage Client, ob Treffer //Kein Treffer? serverDran = false; //ss.spieler.spiel[ss.koordinate3.x][ss .koordinate3.y] =ss.spieler.WASSER; //ss.koordinate3=null; //Doch ein Treffer? ss.schiffZahlClient--; } } //Sind wir immer noch dran? if (ss.schiffZahlClient<=0){ //gewonnen spielEnde=true; return; } } } }}} 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 class Spielfeld { public final int WASSER = 0; public final int SCHIFF = 1; public final int BELEGT = 2; public final int FREI = 3; public final int VERSENKT = 4; public int spiel[][]; Point[] zweier, dreier1, dreier2, vierer, fuenfer; final int[] schiffZahl= {2,3,3,4,5}; public Spielfeld () { spiel = new int[10][10]; //initialisiere Spielfelder als leer for (int i = 0; i<10; i++) 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.*; import java.math.*; public class Schiffe extends Spielfeld { Spielfeld sfeld; public Schiffe (Spielfeld spielfeld) { this.sfeld = spielfeld; zweier = legeSchiffAn(); dreier1=legeSchiffAn(); dreier2=legeSchiffAn(); vierer=legeSchiffAn(); fuenfer=legeSchiffAn(); //Bereite Spielfeldmarkierung vor markiereSchiffe(); } public Point[] legeSchiffAn() { Point[] s = new Point[2]; s[0] = new Point(); s[1] = new Point(); return s; } // Gebe Felder, die in der Initialisierung verwendet //wurden, frei private void markiereSchiffe() { 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); stelleSchiff(4, vierer); stelleSchiff(3, dreier2); stelleSchiff(3, dreier1); stelleSchiff(2, zweier); } 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; double wert; Point ecke = new Point(); while (gesetzt == false) { //Zuerst Ausrichtung des Schiffs festlegen. Ist ein //Zufallswert groesser als 0.5, so horizontale Ausrichtung isHorizontal=((wert = Math.random())>0.5)?true:false; //Linke Ecke des Schiffs if (isHorizontal) { ecke.x = (int)(Math.random()*(10-laenge)-1); ecke.y = (int)(Math.random()*10-1); } else { ecke.y = (int)(Math.random()*(10-laenge)-1); ecke.x = (int)(Math.random()*10-1); } if (ecke.x<0) ecke.x=0; if (ecke.y<0) ecke.y=0; if (schiffPosition(ecke, isHorizontal, laenge)){ gesetzt = true; setzeSchiff(s, ecke, isHorizontal, laenge); } } } 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; int i,j; for (i=0; i<laenge; i++){ 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) ); } return test; } 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; if (richtung) {//horizontal s[0].x = p.x; s[0].y=p.y; s[1].x=p.x+laenge-1; s[1].y=p.y; } else { s[0].x = p.x; s[0].y=p.y; s[1].x=p.x; s[1].y=p.y+laenge-1; } 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 for (i=0; i<laenge; i++) 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 if (richtung) { //horizontal 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.*; public class ServerSpiel extends Spielfeld{ Schiffe reSchiff; Spielfeld spieler, rechner; Point koordinate1, koordinate2, koordinate3; int schiffZahlServer, schiffZahlClient; Im folgenden Konstruktoraufruf werden die Spielfelder initialisiert und die Schiffe des Servers aufgestellt. public ServerSpiel() { //Leere Spielfelder anlegen spieler = new Spielfeld(); rechner = new Spielfeld(); //Schiffe anlegen reSchiff = new Schiffe(rechner); schiffZahlServer=schiffZahlClient=17; reSchiff.stelleAlleSchiffe(); } 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(); do { koordinate1.x = (int)(Math.random()*10-1); koordinate1.y = (int)(Math.random()*10-1); if (koordinate1.x<0) 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); return koordinate1; } 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; //Zuerst den ersten Treffer markieren spieler.spiel[koordinate1.x][koordinate1.y]=spieler. SCHIFF; schiffZahlClient--; if (koordinate2 != null) spieler.spiel[koordinate2.x][koordinate2.y]=spie ler.WASSER; koordinate2=null; // Suchen um koordinate1, wo Schiff weitergeht for (i=-1; i<2;i++) 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(); koordinate2.x=koordinate1 .x+i; koordinate2.y=koordinate1 .y+j; break; } return koordinate2; } private int abs(int i) { 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; int i = 0; if (koordinate3 != null) spieler.spiel[koordinate3.x][koordinate3.y]=spie ler.SCHIFF; koordinate3 = null; //Vorherigen Treffer markieren spieler.spiel[koordinate2.x][koordinate2.y]=spie ler.SCHIFF; schiffZahlClient--; if (koordinate1.y==koordinate2.y) //horizontales 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 (isHorizontal) { if (koordinate1.x+i<10) { if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==FREI){ koordinate3.x=koordinate1.x+i; koordinate3.y=koordinate1.y; return koordinate3; } else if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==WASSER) //Ende des Schiffs erreicht, breche diese Schleife ab i=10; } } else { // Vertikale Richtung if (koordinate1.y+i<10){ 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]==WASSER) //Ende des Schiffs erreicht, breche diese Schleife ab i=10; } } } //Kein Erfolg? Jetzt Suche nach links bzw. nach oben i=0; while (i>-10) { i--; if (isHorizontal) { if (koordinate1.x+i>=0){ if (spieler.spiel[koordinate1.x+i][koordinate1 .y]==FREI){ koordinate3.x=koordinate1.x+i; koordinate3.y=koordinate1.y; return koordinate3; } 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 // Spielregel: Um ein Schiff herum duerfen // keine weiteren unmittelbar sein. 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? public boolean SchiffInDerNaehe(int x, int y) { 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; } return false; } 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? if (stelleTrefferFest(p)) 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; resultat=resultat||treffer(0,p,reSchiff.zweier)||tre ffer(1,p,reSchiff.dreier1)||treffer(2,p,reSchiff.dre ier2)||treffer(3,p,reSchiff.vierer)||treffer(4,p,reS chiff.fuenfer); return resultat; } public boolean treffer(int ind, Point p, Point[] s) { boolean resultat = false; //Horizontale Auswertung if ((s[0].x <= p.x)&&(s[1].x>=p.x)&&(s[0].y==p.y)&&(s[1].y==p.y)) if (rechner.spiel[p.x][p.y]==SCHIFF){ rechner.spiel[p.x][p.y]=VERSENKT; schiffZahlServer--; rechner.schiffZahl[ind]--; resultat=true; } //Vertikale Auswertung if ((s[0].y <= p.y)&&(s[1].y>=p.y)&&(s[0].x==p.x)&&(s[1].x==p.x)) if (rechner.spiel[p.x][p.y]==SCHIFF){ rechner.spiel[p.x][p.y]=VERSENKT; rechner.schiffZahl[ind]--; schiffZahlServer--; resultat=true; } return resultat; } } 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.