WS 2011/2012 WS 2011/2012 Fakultät Angewandte Informatik Programmierung verteilter Systeme 07.11.2011 31.10.11 Prof. Dr. Bernhard Bauer Betreutes Programmieren Programmieraufwand für geübte Programmierer: max. 40 min. Programmieraufwand für ungeübte Programmierer: 50 – 80 min. Musterlösung: 94 Codezeilen1 Forderung: Beschriebene Funktionalitäten ohne Zusatzaufgaben. 4-Gewinnt Grundsätzliches: API Alle von Java unterstützten Funktionen und Klassen finden Sie in der dazugehörigen API (Applications Program Interface). Dazu sollte man wissen, dass alle Funktionen bestimmten Klassen zugeordnet sind. Hinzu kommt noch, dass diese Klassen wiederum in Paketen enthalten sind – allerdings ist dies für die Lösung der Aufgabe irrelevant und wird im Laufe des Semesters vertieft. Dennoch sollten Sie sofort damit anfangen, sich mit der API zu beschäftigen: 1 1 3 3 2 2 Java-API: 1: Alle Pakete; 2: Alle im gerade gewählten Paket enthaltenen Klassen; 3: Alle in der gewählten Klasse enthaltenen Funktionen 1 Orientierung: Keine Leerzeilen; keine Kommentare;{} in eigenen Zeilen (34x); Klammer auch um 1-Zeilen Blocks; keine ?-Operatoren; 1 Anweisung pro Zeile, außer bei Deklarationen gleichen Typs: int a, b, c=0,...; Eclipse (Schnellstart für CIP-Pool) – – – – – – – – – – – – – Starten Sie Eclipse unter C:\Program Files\eclipse\eclipse.exe Richten Sie sich einen geeigneten Workspace ein. Als Default wird Ihnen C:\Users\<name>\workspace empfohlen. In diesem Ordner werden alle Ihre Programme gespeichert. Wählen Sie nun rechts das Symbol "Workbench Go to the Workbench" Gehen Sie nun auf File -> New -> Project wobei Sie das 2. Project wählen Wählen Sie nun unter Java -> Java Project, anschließend Next Wählen Sie nun einen Projektnamen, wie z.B. "Betreutes Programmieren 2" und wählen Sie Finish. Die anschließende Frage können sie mit Yes beantworten Markieren Sie nun im linken Fenster unter Betreutes Programmieren 2 den src - Ordner Anmerkung: Per Konvention ist es üblich, dass jede Java-Klasse sich in einem Paket befindet. Diesen Standard wollen wir uns angewöhnen. Rechtsklick auf src und wählen Sie New->Package und wählen Sie einen geeigneten Namen, wie "main" (Paketnamen werden nach Konvention immer mit Kleinbuchstaben am Anfang geschrieben) Rechtsklick auf main und wählen Sie New->Class und nennen Sie diese "VierGewinnt" Es öffnet sich nun im rechten Teil ein Dokument in dem Sie Ihren Code schreiben können Ausgeführt wird der Code über den weißen Pfeil auf grünem Grund zwischen dem Käfer und dem analogen Pfeil mit einem kleinen roten Koffer darunter oder mit Run->Run oder Strg+F11 oder Rechtsklick auf VierGewinnt->RunAs-> JavaApplication Kommandozeilenparameter kann man übergeben via: Run->Open Run Dialog->(x)=Arguments->Programm arguments: Das ist ein test dies ist das gleich wie die Eingabe java VierGewinnt Das ist ein test in der Eingabeaufforderung und übergibt damit dem Programm die vier Parameter args[0]=Das args[1]=ist args[2]=ein args[3]=test 4-Gewinnt: Beim 4-Gewinnt gibt es ein Spielfeld aus BREITE vielen Schächten, die je HOEHE viele Steinen fassen. Zwei Spieler werfen abwechselnd einen ihrer Steine von oben in einen Schacht ihrer Wahl, wobei der Stein soweit wie möglich nach unten fällt.Gewinner ist derjenige, der als erster 4 Steine – oder mehr – nebeneinander in einer Reihe platzieren kann, wobei hier alle Richtungen (horizontal, vertikal und diagonal) zählen. Zur Veranschaulichung dient folgende Grafik, bei der der Spieler mit den X-Steinen gewonnen hat: | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | X | | | | | | | | | X | O | | | | | | | | X | O | X | | | | | | | X | O | O | O | X | | | | | Aufgabe: Schreiben Sie ein Kommandozeilen-basiertes 4-Gewinnt Spiel. Dabei soll der Mensch gegen die Maschine antreten. Der Einfachheit halber wird auf die automatische Überprüfung, wer gewonnen hat, verzichtet. Schreiben Sie sich eine Klasse VierGewinnt, in der sich auch die Main-Methode befindet. Die Feldgröße (HOEHE, BREITE) wird beim Starten des Programms via Kommandozeilenparameter festgelegt und die Werte als globale Variablen private static int HOEHE und private static int BREITE gespeichert. Dabei soll die Anzahl der übergebenen Parameter überprüft und eine entspr. Fehlermeldung ausgegeben werden. Außerdem legen zwei weitere globale Variablen vom Typ private static char die Spielsteine von SPIELER und RECHNER fest. Plausibilitätstests der Spielfeldabmessungen sind Ihnen freigestellt. In der Main-Methode wird das Spielfeld als ein 2-dimensionales char-Array feld realisiert, welches zu Beginn mit Leerzeichen initialisiert wird. Anschließend wird in einer Endlosschleife zunächst das Spielfeld ausgegeben, der Benutzer zu einer Eingabe aufgefordert, das Spielfeld abermals ausgegeben und die Eingabe des Rechners verarbeitet. Dann startet die Schleife von vorn. Die Ausgabe des Feldes soll mit einer Methode private static void maleFeld(char feld[][]) geschehen. Eine weitere Methode private static leseKoordinate() fordert den Benutzer solange zu einer Eingabe auf, bis dieser entweder einen Wert zwischen 1 und BREITE eingibt oder mit 0 anzeigt, dass er das Programm beenden möchten. Die dritte Methode private static boolean trageEin(char feld[][], int schacht, char stein) trägt in den vom Benutzer oder Rechner gewählten schacht dessen Spielstein stein (globale Variable) in das Spielfeld feld ein und liefert als Rückgabewert, ob das geklappt hat oder nicht. Sollte dies nicht geklappt haben, wird der entsprechende Spieler in der Main-Methode solange wieder zu einer Eingabe aufgefordert, bis ein passender Schacht gewählt wurde, so dass das Eintragen klappt. Der Schacht des Rechners wird berechnet, indem man sich mit einer passenden Funktion der java.lang.Math-Klasse eine Zufallszahl aus [0,1[ generieren lässt, diese auf das Intervall [0,BREITE-1[ streckt und anschließend zum nächstgelegenen int rundet (ebenfalls durch geeignete Funktion der Math-Klasse): eingabe = (int) Math.RUNDEN((BREITE-1) * Math.ZUFALL); (RUNDEN und ZUFALL sind dabei passend zu ersetzen) Da es sich um eine Endlosschleife handelt, muss das Programm unter Umständen mit Strg+C beendet werden. Nach Ihrem bisherigen Wissensstand dürfte es kein Problem darstellen, dieses Programm in C zu schreiben. Wenn Sie sich in Java bereits sicher fühlen oder Erfahrung haben, versuchen Sie, die Aufgabe nach obiger Aufgabenstellung zu lösen. Andernfalls erhalten Sie nun im Folgenden eine Schritt für Schritt Anleitung mit Vergleichen, wie es in C gemacht werden würde, und was für Elemente gerade behandelt werden. Anleitung: Bevor Sie mit der Arbeit beginnen, öffnen Sie die API, scrollen Sie im linken oberen Frame (1) zum Paket java.lang und klicken es an. java.lang ist das Standardpaket und ist immer eingebunden. Im unteren Frame (2) öffnet sich nun eine Übersicht über alle Interfaces, Klassen, Aufzählungen, ... wobei vorerst die Klassen das einzig Interessante für uns sind. Klicken Sie nun auf eine Klasse, z.B. Math, erhalten Sie im rechten Frame (3) eine Übersicht über diese Klasse, bestehend aus Beschreibung, Konstantenübersicht, Methodenübersicht, Detaillierte Konstanten, Detaillierte Methoden. Die Methodenübersicht teilt Ihnen in der ersten Spalte mit, welche Modifikatoren die einzelnen Funktionen haben (diese spielen die ersten paar Wochen keine Rolle) und welchen Rückgabetyp und -wert. In der zweiten Spalte steht dann der Funktionsname mit Eingabeparameterliste und einer kurzen Beschreibung: static double abs (double a) ist eine statische Methode, die den Absolutbetrag einer double-Zahl als double zurückliefert. 1) Wir wollen mit der Main-Funktion starten. Bevor man aber in Java die Main-Funktion, oder irgendeine andere Funktion, benutzen kann, benötigt man eine Klasse, die diese und weitere Funktionen umschließt. Daher definieren wir uns also eine Klasse mit einem sprechenden Namen: public class VierGewinnt - Per Konvention schreibt man Klassennamen immer groß. - public ist ein Modifikator, der dafür sorgt, dass die Klasse von überall aus erreichbar ist. Mehr dazu im Laufe des Semesters. Speichern Sie die Datei auch unbedingt unter dem Klassennamen ab, da es sonst nicht möglich ist, das Programm zu kompilieren. In diesem Fall muss Ihre Datei also VierGewinnt.java heißen. Alle Funktionen werden in dieser Klasse zusammengefasst, also erhalten wir: public class VierGewinnt { // Hier kommen Funktionen hin } 2) Während es in C verschiedene Signaturen der Main-Funktion gibt, hat Java nur eine: public static void main (String args[]), die ein wenig an int main (int argc, char* argv[]) erinnert. - public und static sind Modifikatoren, die für die ersten paar Wochen des Semesters einfach akzeptiert werden sollten ;) - Im Gegensatz zur C-Main gibt die Java-Main keinen Wert zurück und ist somit void. - Sowohl in Java, als auch in C werden die Argumente als Strings übergeben, allerdings wird in Java kein int argc benötigt, da man die Länge des String-Arrays args mittels args.length abfragen kann und args[0] beinhaltet nicht den Programmnamen, sondern den ersten Kommandozeilenparameter – sofern vorhanden. Wir erhalten also: public class VierGewinnt { public static void main (String args[]) { } } 3) Nutzt man in C globale Variablen, so müssen diese vor deren Benutzung in der Funktion deklariert werden, Java hingegen sieht eine Klasse als abgeschlossenes System und lässt Deklarationen überall zwischen den Funktionen zu. Dennoch wollen wir uns an die Konventionen halten und globale Variablen am Anfang der Klassendeklaration vor der ersten Funktion deklarieren: public class VierGewinnt { // Platz für globale Variablen public static void main (String args[]) { } } Für das Programm benötigen wir 4 globale Variablen: int HOEHE, int BREITE für die Spielfeldabmessungen und char SPIELER und char RECHNER, wobei der Spieler mit dem Stein 'X' und der Rechner mit den Stein 'O' spielen soll. Als Modifikatoren benötigen die Variablen außerdem noch private static. So könnten also zwei der vier Variablen private static int HOEHE; private static char SPIELER='X'; lauten (die anderen beiden schaffen Sie selbst). 4) Gut, der Rahmen wäre also geschaffen. Da die Feldgröße entscheidend für das Spiel ist und über Kommandozeilenparameter eingegeben werden soll, wird dies der erste Schritt sein. Aus C kennen Sie das Vorgehen: if(argc<3) { printf("Zu wenig Parameter. Bitte geben Sie Höhe und Breite an."); return 1; } if(argc>3) { printf("Zu viele Parameter. Bitte geben Sie nur Höhe und Breite an."); return 2; } Übersetzung: Wie Sie aus dem HelloWorld - Programm der Vorlesung bereits wissen, können Sie in Java mittels System.out.println("") Text auf der Konsole ausgeben. Was argc in Java heißt, wurde bereits in 2) geklärt, und einen Programmabbruch erzeugt man mit der exit() Methode der Klasse System2, also mittels System.exit(0). 5) Nun, da wir wissen, dass wir die richtige Anzahl an Parametern bekommen haben, können wir diese verarbeiten. Allerdings gibt es dabei noch ein kleines Hindernis: args ist ein String-Array und die globalen Variablen BREITE und HOEHE, in denen die Abmessungen gespeichert werden sollen, sind vom Typ int, d.h. wir müssen noch den int-Wert parsen. Dies wird mit der Methode parseInt() der Hüll-Klasse Integer gemacht: HOEHE = Integer.parseInt(args[0]); (BREITE analog mit args[1]) Achtung: Im Gegensatz zu C wird in Java die Fehlerbehandlung nicht unbedingt über Rückgabewerte geregelt. Würden Sie in diesem Fall einen String parsen, der nicht ausschließlich aus Integer besteht (und ggf. noch dem Minus-Zeichen), so würde eine sog. NumberFormatException geschmissen werden. Dazu erfahren Sie im Laufe des Semesters noch mehr und dies soll hier nur als Ausblick erwähnt werden. Daher ergeben auch Plausibilitätstests der Variablen an dieser Stelle wenig Sinn und können somit weggelassen werden. 6) Zu Testzwecken können wir mal den ersten Testlauf machen und uns dabei die über die Kommandozeile übergebenen Parameter ausgeben lassen. In C wurde dies mittels printf("Eingegeben wurden HOEHE %i und BREITE %i", HOEHE, BREITE) gemacht. In Java ist es möglich Strings, oder Variablen anderen Datentyps und Strings mit + zu verbinden: "Das ist ein Test "+4 würde "Das ist ein Test 4" liefern. Oder wenn man eine Variable int anzahl hat, so könnte man den Satz System.out.println("Sie haben "+anzahl+" gelbe Bananen bestellt") ausgeben lassen. Machen Sie dies für HOEHE und BREITE. Um das Programm auszuführen, müssen Sie es zunächst kompilieren: javac VierGewinnt.java Anschließend führen Sie es mit java VierGewinnt 4 7 oder anderen Parametern aus und Sie sollten ihre Ausgabe ähnlich "HOEHE: 4, BREITE: 7" 2 System enthält einige Methoden des laufenden „Systems“, also der Java Virtual Machine. Schauen Sie sich bei Zeiten mal die Methode gc() genauer an und versuchen Sie sich dadurch zu erklären, warum es in Java keine free() Methode gibt. erhalten. 7) Als nächstes definieren wir uns ein Spielfeld, das aus einem 2-dimensionalen char-Array besteht. Während dies in C dynamische Speicherreservierung mittels malloc() bedeutete, ist dies in Java wesentlich einfacher zu lösen. Wir erinnern uns: War die Array-Größe bereits zur Kompilierzeit bekannt, so konnte man in C ein 2-dim Array mittels char feld [BREITE][HOEHE] deklarieren. Java ermöglicht dies auch zur Laufzeit, allerdings werden Arrays und andere Objekte mit new erzeugt, was man mit der dynamischen Speicherreservierung vergleichen kann. So erhält man: char feld[][] = new char[BREITE][HOEHE] Initialisieren Sie nun mit einer doppelten for-Schleife (o.ä.) das Spielfeld mit Leerzeichen ' '. Merke: Alle mit new erzeugten Objekte verhalten sich wie Pointer in C, folgen also in Funktionen dem Call by Reference Prinzip! 8) Um das Spiel zu realisieren, lagern wir die restlichen Aufgaben in drei Funktionen aus: private static void maleFeld (char feld[][]) private static int leseKoordinate() private static boolean trageEin(char feld[][], int schacht, char stein) anders als in C ist es nicht nötig, Prototypen der Funktionen am Anfang der Klasse zu deklarieren. Wir haben jetzt also public class VierGewinnt { // Globale Variablen public static void main (String args[]) { // args-Prüfung // feld-Deklaration + Initialisierung } private static void maleFeld (char feld[][]) {} private static int leseKoordinate() {} private static boolean trageEin(char feld[][], int schacht, char stein) {} } 9) Als erstes widmen wir uns der maleFeld() – Funktion: private static void maleFeld (char feld[][]) Dabei soll das übergebene feld Zeile für Zeile durchlaufen werden. Die Funktion ist void, d.h. sie gibt nichts zurück. Die Abmessungen von feld sind durch HOEHE und BREITE gegeben.3 Weiter sei noch gesagt, dass System.out.println() die Ausgabe mit einem '\n' abschließt, System.out.print() hingegen nicht. Sie können an die Funktionen Strings mit "" übergeben oder einzelne Character / Steuerzeichen mit einfachen Hochkommata ''. Wie bei printf eben auch. Ansonsten müssen Sie nur mit einer for-Schleife feld durchlaufen und den Inhalt (angemessen formatiert, z.B. so wie in der Beschreibung zu Anfang) ausgeben. In Java ist auch die Deklaration von Variablen in der for-Schleifen-Definition erlaubt: for (int i=0;i<bla;i++). Ansonsten ist die Aufgabe analog zu C lösbar. Bemerkung: Geben Sie das Feld besser von HOEHE-1 bis 0 aus, so dass auch tatsächlich die Felder von „unten“ nach „oben“ befüllt werden. Ansonsten würde ja die 0te Zeile zuerst, dann die 1., die 2. usw. ausgegeben, sodass obige Abbildung auf dem Kopf stünde. Anmerkung: Auch in Java gäbe es printf. 10) Die zweite Funktion ist leseKoordinate(): private static int leseKoordinate() Sie soll den Benutzer so lange zu einer Eingabe auffordern, bis dieser eine gültige Eingabe liefert und diese dann zurückgeben. „Gültige Eingabe“ bedeutet, dass die eingegebene Zahl in [1;BREITE] liegen soll. Definieren Sie 0 als Eingabe für den Abbruch des Programms. In C haben Sie gelernt: int leseKoordinate() { int x=-1; do { printf("Bitte eine Zahl zwischen 1 und %i eingeben",BREITE); x = getchar(); while(getchar()!='\n' && x!='\n'); }while(y-48<0 || y-48>BREITE)) if(y-48==0) exit(0); return y-48; } Eine der Möglichkeiten, um in Java von Kommandozeile einzulesen ist die System.in.read() Methode. Diese liest das erste Zeichen des Eingabestroms aus, bei nochmaligen Aufruf das zweite usw. Diese Methode ist also analog zu getchar(). Leider ist es hier aufgrund des Exception-Handlings in Java notwendig eine von System.in.read() geschmissene Exception abzufangen. Ohne sich weiter darüber Gedanken zu machen, können Sie die Methode wie folgt aufbauen: private static int leseKoordinate() { int y = -1; 3 Eigentlich etwas unsauber. Man kann die Länge der Felder auch mit feld.length und feld[0].length abfragen. try { // Code Ihrer Funktion } catch(Exception e){} return y; } Ansonsten ist die Aufgabe analog zu C lösbar. Auch hier muss der Eingabestrom geleert werden. 11) Die letzte Funktion ist trageEin(): private static boolean trageEin(char feld[][], int schacht, char stein) Dabei werden Sie mit dem neuen Datentyp boolean vertraut gemacht. Dieser hat als Wertemenge lediglich {true,false}. In der Funktion soll versucht werden, den Spielstein stein in den durch schacht festgelegten Schacht des Spielfelds feld einzutragen. Da ja von oben in das Spielfeld geworfen wird, muss überprüft werden, ob in diesem Schacht überhaupt noch Platz ist und falls ja, muss der Stein auf dem derzeit höchsten Stein des Schachts platziert werden. Ist das Eintragen erfolgreich verlaufen, soll true zurückgegeben werden, ansonsten false. In C wäre das ... int i=0; while(i<HOEHE && feld[schacht][i]!=' ') {i++;} if(i<HOEHE) { feld[schacht][i] = zeichen; return 1; } return 0; 12) Nachdem wir nun alle Funktionen be- und geschrieben haben, können wir zur Main-Funktion zurückkehren. Deklarieren Sie sich dort eine Variable boolean gewonnen und initialisieren Sie diese mit false, sowie eine Variable int eingabe. Fügen Sie in den bisherigen Code eine Endlosschleife while(!gewonnen) { // hier den restlichen Code } ein. In dieser Endlosschleife soll nun abwechselnd das gleiche passieren: - geben Sie das Spielfeld mittels des Aufrufs maleFeld(feld) aus. - in einer do-while-Schleife soll nun der Rückgabewert von leseKoordinate() in eingabe gespeichert werden. Als Abbruchbedingung wählen Sie den Rückgabewert true der Funktion trageEin(feld,eingabe,SPIELER). Beachten Sie, dass der Spieler Zahlen aus [1;BREITE] eingibt, das Array feld allerdings die Schächte [0;BREITE-1] besitzt: do { eingabe= ...; }while(trageEin(...)==false); - anschließend wird wieder feld ausgegeben. - nun folgt eine weitere do-while-Schleife, in der der Rechner seinen Zug macht. eingabe wird dabei mittels der Rundung zum nächsten Integer einer double Zufallszahl berechnet. Hier dürfen Sie etwas die Arbeit mit der API üben. Die Klasse Math in java.lang bietet eine Vielzahl an mathematischen Funktionen. Eine Funktion liefert eine double-Zufallszahl zwischen 0 und 1, eine andere Funktion rundet ein double zum nächstgelegenen Integer und gibt diese Zahl als double zurück. Sie muss folgerichtig gecastet werden. Haben Sie die beiden Funktionen gefunden, können Sie diese hier einsetzen (wobei Math. benötigt wird): eingabe = (int) Math.RUNDEN((BREITE-1) * Math.ZUFALLSZAHL); Diese do-while-Schleife hat dieselbe Abbruchbedingung wie die vorhergehende. Anschließend beginnt die Endlosschleife von vorn. Damit ist das Programm abgeschlossen. Die Endlosschleife kann mittels Strg+C abgebrochen werden. Freiwillige Zusatzaufgaben: – – – Schreiben Sie eine Funktion private static boolean istVoll(char feld[][]), die überprüft, ob ein Spielfeld bereits voll ist. Implementieren Sie eine Funktion private static boolean hatGewonnen(char feld[][]), die überprüft, ob ein Spieler gewonnen hat. Dabei muss überprüft werden, ob waagerecht, senkrecht oder diagonal 4 oder mehr Steine vom gleichen Typ in einer Reihe liegen. Es empfiehlt sich diese Überprüfungen wiederum in eigene Funktionen auszulagern (testeWaag, testeSenk, testeDiag). Außerdem spart man sich Arbeit, wenn man nur vom aktuell gesetzten Stein aus prüft und nicht das ganze Feld. Der Zufallsgenerator ist als Gegner keine Herausforderung. Überlegen Sie sich eine lustige künstliche Intelligenz public static int kI()