In Verlauf des Semesters wollen wir ein Memory-Spiel programmieren, das sich dem Spieler etwa so präsentieren soll wie auf der folgenden Bildschirmkopie: Zu den Regeln des Memory-Spiels siehe z.b. http://de.wikipedia.org/wiki/memory_(spiel) Natürlich fällt es einem ungeübten Programmierer nicht leicht, ein solches Spiel einfach aus dem Stand zu realisieren. Aber, um den chinesischen Weisen Lao Tse zu zitieren: Auch der längste Marsch beginnt mit dem ersten Schritt. Das bedeutet für uns: Wir überlegen, aus welchen lösbaren Teilaufgaben sich die zunächst als zu komplex erscheinende Aufgabe zusammensetzt: Für ein Memory-Spiel benötigen wir Kärtchen, insbesondere die Bilder auf den Vorder- und Rückseiten, was zur ersten Aufgabe führt: Ein kleines Zeichenprogramm für Memory-Kärtchen. Wir benötigen eine Möglichkeit, die mit unserem Programm erstellten Bilder in Dateien zu speichern und von dort wieder in ein Programm einzulesen: Das wird unsere Aufgabe 2a sein. Wir müssen eine Memory-Karte auf dem Bildschirm darstellen und mit der Maus umdrehen können, damit ihre Vorderseite sichtbar wird: Das ist unsere Aufgabe 2b. Das Memory-Spiel besteht aus einem ganzen Stapel solcher Kärtchen. Wir wollen die Kärtchen mit den vorher erstellten Bildern erzeugen und zufällig auf unsere Spielfläche verteilen: Das ist unsere Aufgabe 3. Schließlich wollen wir Memory spielen, dazu müssen wir in der letzten Aufgabe 4 folgende Regeln implementieren: Zwei Kärtchen werden umgedreht; zeigen sie das gleiche Bild, gibt es einen Bonus, zeigen sie verschiedene Bilder, gibt es einen Malus. Beides schlägt sich in der Gesamtpunktzahl Total Score nieder. Kärtchen mit verschiedenen Bildern müssen wieder umgedreht werden, bevor weitergespielt werden kann. Hinweis: Zur Lösung der folgenden Übungsaufgaben ist es absolut sinnvoll, zunächst die Kapitel 11, 14 und 15 der Vorlesung eingehend zu wiederholen - nur so werden Sie in der Lage sein, die gegebenen Hinweise zu verstehen
Aufgabe 1 In unserer ersten Aufgabe geht es im wesentlichen um das einfache Zeichenprogramm CardPainter für die Memory-Karten-Motive. Das Programm begnügt sich mit der Erstellung von 300 x 300 Pixel großen Bitmaps (schwarz/weiß) und einer einfachen Bedienoberfläche wie folgt: In einem Fenster mit Titel Karten zeichnen werden angeboten: Ein Zeichenbereich (300x300, CENTER), ein Slider (EAST), zwei Buttons (SOUTH). Der Slider erlaubt die Einstellung der Strichdicke beim Zeichnen zwischen 0 und 30 Pixel. Im Zeichenbereich werden bei gedrückter Maustaste schwarze Kreise entsprechend der gewählten Strichdicke gezeichnet. Der Clear-Button dient zum Löschen einer misslungenen Zeichnung. Der Bild-Speichern-Button hat zunächst keine Funktion. In der folgenden Aufgabe 2 soll er einen Speichern-Dialog für das erstellte Bild starten. Auch diese erste (Teil-) Aufgabe erscheint sehr umfangreich. Die Frage ist, wie können wir hier eine Zerlegung in noch kleinere Schritte vornehmen? Wie würden Sie vorgehen? Im Folgenden wird eine Vorgehensweise vorgeschlagen und mit Hinweisen zur Realisierung der einzelnen Schritte versehen: Wir erstellen eine Klasse MyPoint, deren Objekte Koordinaten x, y und Radius r eines zu zeichnenden (sehr fetten) Punktes aufnehmen können. Diese Klasse soll auch fähig sein, sich auf ein Graphics zu zeichnen, d.h. eine Methode draw(graphics g) bieten, die das leistet (vgl. Kapitel 14 der Vorlesung). Die Klasse MainWindow erbt von JFrame und wird die o.g. GUI-Elemente präsentieren. Der Behälter contentpane eines JFrames (man erhält in mit den Aufruf getcontentpane()) dient zur Aufnahme von Java-Components, in unserem Beispiel von Objekten spezialisierter Klassen wie JPanel oder JComponent (ganz nach Geschmack für die Zeichenfläche), JButton (dem Button) und JSlider (dem Slider rechts). Einfügen von Components erfolgt mit Hilfe der Methode add() der contentpane, die vom Typ Container ist. Das Layout der
eingefügten Komponenten übernimmt ein LayoutManager, in der contentpane ist das standardmäßig ein BorderLayout. Bei allem hilft Abschnitt 11 der Vorlesung. Die Zeichenfläche im Zentrum des Fensters realisieren Sie mit einer eigenen Klasse MyCanvas, die von JPanel erbt. Diese Klasse muss auf Mausereignisse reagieren können, damit das Erzeugen von Objekten der Klasse MyPoint bzw. das Zeichnen derselben gelingt. Dazu sollten Sie eine interne Klasse MyMouseListener bereitstellen, die von MouseAdapter (siehe die Java-Dokumentation und Kapitel 15.1 der Vorlesung) erbt und die Methoden mousepressed() und mousedragged() geeignet überschreibt: Bei diesen Mausereignissen soll ein MyPoint- Objekt erzeugt werden, und MyCanvas sollte seinen Inhalt und damit alle bisher erzeugten Punkte neu zeichnen (siehe Vorlesung 14.1). Ein MouseAdapter implementiert beide benötigten Interfaces MouseListener und MouseMotionListener (jedenfalls seit Java 7, in früheren Versionen braucht es noch einen MouseMotionAdapter). Die Anmeldung eines Objekts vom Typ MyMouseListener bei MyCanvas erfolgt dann über die Methodenaufrufe addmouselistener(mymouselistener) a l s MouseListener b z w. addmousemotionlistener (mymouselistener) als MouseMotionListener. Da die Zeichnung eine Auflösung von 300x300 Pixeln haben soll, ist im Konstruktor ein Statement wie folgt sinnvoll: setpreferredsize(new Dimension(imageWidth,imageHeight)); Da eine Zeichnung aus vielen Punkten vom Typ MyPoint besteht, sollten Sie nun einen Punktbehälter MyPointContainer vorsehen und dafür sorgen, dass bei einem Neuzeichnen alle Punkte des Behälters zum Zeichnen aufgefordert werden. Das geschieht am besten im Behälter selbst, d.h. auch hier sollte eine Methode draw(graphics g) bereitstehen. Der Slider rechts ist vom Typ PaintSlider, eine eigene Klasse, die von JSlider erbt (hier könnten Sie die Java-Dokumentation konsultieren). Seine Gestalt in obiger Abbildung erhält er z.b. durch einen Konstruktor public PaintSlider(){ super(jslider.vertical, SIZE_MIN, SIZE_MAX, SIZE_INIT); //Turn on labels at major tick marks. this.setmajortickspacing(10); this.setminortickspacing(1); this.setpaintticks(true); this.setpaintlabels(true); } Den Inhalt des Sliders kann man entweder direkt abfragen (getinitsize()) oder man kann sich bei ihm als ChangeListener anmelden (addchangelistener(changelistener l)), um bei jeder Änderung benachrichtigt zu werden. Einen ChangeListener implementiert man hier vielleicht so: class SliderChangeListener implements ChangeListener{ public void statechanged(changeevent e) { PaintSlider source = (PaintSlider) e.getsource(); if (source.getvalueisadjusting()) { int size = (int) source.getvalue(); mycanvas.setpensize(size); } } } Das Programm berücksichtigt nun den Slider-Wert bei der Erzeugung neuer Punkte, wenn Sie die Methode setpensize(int size) sinnvoll implementieren. Der Clear-Button sollte bei Betätigung alle bisher erzeugten Punkte verwerfen. Es gelingt Ihnen, einen zweiten Button nach BorderLayout.SOUTH Ihres Hauptfensters zu bringen, wenn Sie zunächst ein JPanel in SOUTH legen und für dieses ein FlowLayout vereinbaren. Schließlich erreichen Sie eine gute Darstellung zum Programmstart und verhindern eine Größenänderung Ihres Hauptfensters mittels pack(); setresizable(false);
Aufgabe 2 Wir wollen in dieser Aufgabe das Zeichenprogramm aus Aufgabe 1 erweitern, um erstellte Bilder speichern zu können und ein Programm Memory-Spiel (Version 0.1) erstellen, mit zunächst folgenden Features: Das Programm präsentiert eine einzige Karte auf dem Bildschirm. Die Karte soll 2 Seiten haben, deren Bilder (je eines für die Vorder- bzw. Rückseite) von Dateien geladen werden können. Wir gehen davon aus, dass wir die Bilder vorher mit dem Zeichenprogramm erstellt haben. Mittels Mausklick auf die Karte soll diese umgedreht werden können (d.h. abwechselnd Rück- und Vorderseite zeigen). Hinweise zur Realisierung: Beim Speichern der Zeichnung benötigen wir zunächst ein Bild, das wir speichern können. Theoretisch könnten wir Java veranlassen, schlicht eine Bildschirmkopie anzufertigen. Wir können aber auch so vorgehen, dass wir das speicherbare Bild (ein BufferedImage) einfach selbst zeichnen - ganz genau so, wie wir in der Methode paintcomponent() auf das JPanel gezeichnet haben: // BufferedImage mit der richtigen Größe erzeugen BufferedImage image = new BufferedImage(imageWidth,imageHeight, BufferedImage.TYPE_INT_BGR); // Graphics von image holen Graphics g = image.getgraphics(); // Farbe für einen schönen grauen Hintergrund wählen g.setcolor(new Color(230,230,230)); // Hintergrund zeichnen g.fillrect(0,0,imagewidth,imageheight); // Farbe schwarz wählen g.setcolor(color.black); // jetzt Punkte auf g zeichnen wie bisher... Nun wollen wir den Benutzer einen Dateinamen wählen lassen. Dies gelingt mit der Klasse JFileChooser, die einen Speichern-Dialog liefert. Einen Dialog, der im aktuellen Directory mit der Auswahl beginnt, erzeugt man mittels JFileChooser savefilechooser = new JFileChooser("."); Den Aufruf des Dialogs und die Abfrage des gewählten Dateinamens erreicht man wie folgt: savefilechooser.showsavedialog(this); File file = savefilechooser.getselectedfile(); Sorgen Sie weiter dafür, dass bei der Wahl eines existierenden Files dieser nicht überschrieben wird. Ändern Sie in diesem Fall den Dateinamen ab, indem Sie das Wort Kopie anhängen. Wahlweise ist natürlich auch ein neuer Dialog mit dem Anwender möglich. Auch können Sie sicherstellen, dass der Filename mit.png endet. Das Schreiben des Bildes im png-format auf den gewählten File file erfolgt danach einfach durch ImageIO.write(image, "PNG", file); Für das Memory-Spiel erstellen wir zunächst ein neues Projekt. Hier wollen wir in einem JFrame eine Memory-Karte präsentieren. Da diese Karte auf Mausklicks reagieren soll, bietet es sich an, sie als speziellen JButton zu realisieren, d.h. wir erstellen eine Klasse MemoryCardButton, die von JButton erbt. Zum Einlesen eines Bildes von einer Bilddatei können wir die Klasse ImageIcon nutzen, die das Interface Image implementiert und uns sehr bequeme Konstruktoren bietet, etwa den folgenden, dem wir nur den Pfadnamen der Bilddatei übergeben müssen: ImageIcon frontimage = new ImageIcon(String dateinamevorderseite);
Vom ImageIcon können wir uns das eingelesene Bild geben lassen mittels Image frontimage = frontimage.getimage(); Schreiben Sie einen Konstruktor der Klasse MemoryCardButton, dem man zwei Pfadnamen für Bilddateien (Vorder- und Rückseite) übergeben kann. Dazu sollten die Klasse natürlich auch zwei Bildattribute besitzen; die Referenz image verweist auf das aktuell angezeigte Bild. Welche Seite der Karte sollte das am Anfang sein? Unser MemoryCardButton kann sich selbst (d.h. das Bild auf seiner Vorder- oder Rückseite) zeichnen, indem er die geerbte Methode paintcomponent(graphics g) entsprechend überschreibt: public void paintcomponent(graphics g) { super.paintcomponent(g); g.drawimage(image, 0, 0, getwidth(), getheight(), null); } Sorgen Sie nun dafür, dass die Referenz image auf das aktuell angezeigte Bild zwischen den Bildern wechselt, wenn der Button gedrückt wird. Dies gelingt, indem Sie einen entsprechenden ActionListener implementieren und bei MemoryCardButton anmelden (vgl. Vorlesung Abschnitt 11.5.1).
Aufgabe 3 In dieser Aufgabe wollen wir die gewünschte Anzahl von Memory-Karten auf die Spielfläche austeilen. Das soll natürlich auf zufällige Weise geschehen, sodass die Spielerin oder der Spieler zunächst nicht weiß, wo welche Karte liegt. Auch sollten wir dafür sorgen, dass jede Karte genau 2 mal vorkommt - genauer: Jedes verwendete Motiv einer Kartenvorderseite sollte genau zwei mal vorkommen, jede Kartenrückseite sollte gleich aussehen. Hier können wir zunächst 3 Probleme identifizieren: Die Karten müssen auf irgendeine Weise vom Programm erzeugt werden. Die vorgegebene Anzahl von Karten soll in zufälliger Anordnung auf das Spielfeld kommen. Das lässt sich realisieren, indem man einen Stapel von Karten zunächst mischt und danach eine Karte nach der anderen auf das Spielfeld legt oder die Karten aus einem nicht gemischten Stapel auf zufällige Weise zieht, sodass eine zufällige Reihenfolge erst auf dem Spielfeld entsteht. Die Karten bzw. die KartenButtons sollen in einer rechteckigen Anordnung auf dem Bildschirm erscheinen. Hinweise zur Realisierung: Schreiben Sie zunächst eine Klasse MemoryCard, die als Attribut den Pfadnamen eines Vorderseitenbildes besitzt. Weiter benötigen wir einen Kartenstapel MemoryCardDeck, der in der Lage ist, sich zu füllen und Karten auf zufällige Weise auszugeben. Zum Füllen des Stapels (für jedes Motiv 2 Karten) soll eine Methode autofill(string partoffilename, int numberofcards) zur Verfügung stehen. Die Karten könnten in einer ArrayList aufbewahrt werden. In einer ersten Version dürfen Sie davon ausgehen, dass partoffilename etwa Vorderseitenbild enthält und im aktuellen Verzeichnis ausreichend viele Bilddateien in der folgenden Form vorliegen: Vorderseitenbild001.png Vorderseitenbild002.png Zum Ziehen zufälliger Karten können Sie die Klasse Random verwenden. Ein Objekt dieser Klasse bietet Ihnen zum Beispiel die Methode int nextint(int n), die Ihnen eine zufällige ganze Zahl zwischen 0 und n-1 liefert. Die Klasse ArrayList bietet andererseits die Methode remove(int index), die die Karte beim index aus der ArrayList entfernt (und diese danach neu durchnummeriert). Natürlich müssen Sie nach dem Ziehen einer Karte die Größe n korrigieren. Für jede dieser gezogenen Karten sollten Sie nun einen MemoryCardButton erzeugen und alle Karten (genauer die Karten-Buttons) mit Hilfe eines GridLayouts (siehe Vorlesung 11.4.8 oder die Java-Dokumentation) auf dem Spielfeld anordnen.
Aufgabe 4 Als letztes sind noch die Spielregeln zu implementieren, damit gespielt werden kann. Weiter sollte unterhalb des Spielfelds ein Label mit dem Text Total Score zeigen, wie viele Punkte der Spieler gerade hat. Es liegt ein wenig an Ihnen, wie Sie die Punkte im Detail vergeben. Zur Implementierung der Spielregeln sollten Sie eine Klasse GameController vorsehen, die folgendes überwacht: Bei Spielbeginn sind alle Karten zugedeckt, das heißt mit den Rückseiten sichtbar auf dem Spielfeld. Eine Karte kann durch Mausklick aufgedeckt (Vorderseite sichtbar) und durch einen weiteren Mausklick wieder zugedeckt werden. Jedes Zudecken einer Karte erzeugt einen Minuspunkt (Malus) für den Total Score. 2 aufgedeckte Karten passen zueinander (wir sagen in kurzem denglish, sie matchen von to match ), wenn sie dasselbe Motiv zeigen. Karten, die matchen, erzeugen einen Bonus für den Total Score und können anschließend nicht mehr zugedeckt werden. Sie dürfen diese Karten natürlich auch ganz entfernen - in diesem Fall sollten Sie aber die Lage der restlichen Karten nicht verändern, sonst gibt es Ärger mit der Kundschaft In einem Spielzug kann eine Spielerin oder ein Spieler maximal 2 noch nicht gematchte Karten aufdecken. Vor einem weiteren Zug müssen diese Karten wieder zugedeckt werden - mit entsprechenden Minuspunkten. Das Spiel endet, wenn alle Kartenpaare gematcht sind.