PD Dr. David Sabel Institut für Informatik Fachbereich Informatik und Mathematik Johann Wolfgang Goethe-Universität Frankfurt am Main Einführung in die funktionale Programmierung Wintersemester 2015/2016 Aufgabenblatt Nr. 4 Abgabe: Montag 9. November 2015 vor der Vorlesung Senden Sie bitte Ihren Quellcode auch per Email an [email protected] Ziel dieses Aufgabenblattes ist es, eine Variante von Mine Sweeper“ in Haskell zu implementieren1 . ” Dabei sind Teile des Codes schon vorgegeben. Die meisten zu implementierenden Funktionen sind Funktionen auf Listen in Haskell. Das Spiel: Das Spielfeld ist eine quadratische Matrix, wobei jeder Eintrag eine Zelle darstellt, die entweder leer oder mit einer Mine versehen ist. Jedoch sind alle Zellen am Anfang des Spiels verdeckt, d.h. es ist unbekannt, wo die Minen liegen. Ziel des Spiels ist es, alle leeren Zellen sichtbar zu machen ohne jemals eine Mine aufzudecken2 : • Wird eine Zelle mit einer Mine aufgedeckt, so ist das Spiel verloren. • Wird eine leere Zelle aufgedeckt, so wird der Inhalt der Zelle und gleichzeitig der Inhalt aller ihrer Nachbarzellen sichtbar. Hierbei wird für sichtbare Nicht-Minen-Zellen eine Zahl angezeigt: Die Anzahl der Minen in der Nachbarschaft. Desweiteren besteht die Möglichkeit nicht-sichtbare Zellen zu markieren (mit einem Fähnchen) und diese Markierung auch wieder zu entfernen. Ein Eintrag mit Index (i, j) in der Matrix bezeichnet die Zelle in Spalte i und Zeile j, wobei von 0 angefangen wird zu zählen. Ein Beispiel zur Indizierung zeigt Abbildung 1 (c). Abbildung 1 (a) zeigt ein mögliches Spielfeld, wobei alle Zellen sichtbar gemacht sind, Abbildung 1 (b) zeigt eine typische Spielsituation: Die Zelle mit Index (3,0) wurde aufgedeckt, dadurch wurden die Zellen mit Index (2,0), (3,0), (2,1) und (3,1) sichtbar, außerdem hat der Spieler die Zellen mit Index (0,0) und (3,2) markiert. Repräsentation in Haskell: Zur Repräsentation des Spiels in Haskell verwenden wir die folgenden Datentypen: Eine Matrix ist eine Liste von Listen von Entry-Objekten 1 Der hier verwendete Quellcode ist auch auf der Webseite der Veranstaltung zu finden. Beachten Sie, dass wir hier zwischen Sichtbar-machen und Aufdecken unterscheiden: Aufdecken bedeutet, dass der Spieler die Zelle öffnet (und verliert, wenn sich in dieser Zelle eine Mine verbirgt), während Sichtbar-machen nur bedeutet, dass der Inhalt der Zelle angezeigt wird). 2 (0,0) (1,0) (2,0) (3,0) (0,1) (1,1) (2,1) (3,1) (0,2) (1,2) (2,2) (3,2) (0,3) (1,3) (2,3) (3,3) (a) (b) (c) Abbildung 1: Das MineSweeper-Spielfeld: (a) komplett sichtbares 4 × 4 Spielfeld, (b) rin teilweise aufgedecktes Spielfeld mit Markierungen, (c) Indizierung der 4 × 4 Matrix 1 type Matrix = [[Entry]] data Entry = Marked | Unmarked | Open Int | Mine deriving(Show) Hierbei repräsentiert das Entry-Objekt die aktuelle Anzeige: • Marked: Die Zelle ist verdeckt und mit einer Markierung versehen. • Unmarked: Die Zelle ist verdeckt und nicht markiert. • Open Int: Die Zelle ist sichtbar, enthält keine Mine, aber die Anzahl an Minen in der Nachbarschaft. • Mine: Die Zelle ist sichtbar und enthält eine Mine. Die Matrix aus Abbildung 1 (b) wird dementsprechend als exampleMatrix = [[Marked, Unmarked,Open 2, Open 1] ,[Unmarked,Unmarked,Mine, Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] als Objekt vom Typ Matrix repräsentiert. Ein Spielzustand besteht aus der aktuell angezeigten Matrix, einer Liste derjenigen Koordinaten, die eine Mine enthalten und dem Gewinnzustand. Dies wird durch den Datentyp GameState repräsentiert, der die Record-Syntax verwendet. Für den Gewinnzustand wird dabei der Aufzählungstyp State verwendet, wobei Won bedeutet, dass der Spieler gewonnen hat, Lost, dass der Spieler verloren hat und Undecided, dass das Spiel noch im Gange ist. Für Koordinaten verwenden wir das Typsynonym Coordinates: data GameState = GameState { matrix :: Matrix, mines :: [Coordinates], state :: State } type Coordinates = (Int,Int) data State = Won | Lost | Undecided Die Spielsituation in Abbildung 1 (b), wobei die Minen entsprechend Abbildung 1 (a) platziert sind, wird daher als exampleState = GameState {matrix = exampleMatrix, mines = [(0,0),(1,0),(2,1),(1,3),(3,3)], state = Undecided } repräsentiert. Schließlich gibt es drei mögliche Aktionen durch den Spieler: Er kann eine Zelle aufdecken, oder er kann eine Markierung setzen oder entfernen. Es reicht jedoch aus das Setzen/Entfernen der Markierung durch eine Aktion zu repräsentieren, die wir mit Toggle bezeichnen. Wir führen für Spieleraktionen daher den Datentyp Action ein: data Action = Toggle Coordinates | OpenEntry Coordinates 2 Aufgabe 1 (50 Punkte) Implementieren Sie im Modul MineSweeper a) eine Funktion (!!!) :: Matrix -> Coordinates -> Entry, die eine Matrix und Koordinaten erhält und die Belegung der Zelle mit den Koordinaten berechnet. (5 Punkte) Beispiele: *Main> exampleMatrix !!! (2,0) Open 2 *Main> exampleMatrix !!! (1,3) Unmarked b) eine Funktion updateMatrix :: Matrix -> Coordinates -> Entry -> Matrix, die eine Matrix m, Koordinaten (x, y) und einen Eintrag e erhält und den Inhalt der Zelle mit den Koordinaten (x, y) in der Matrix m auf den Wert e abändert. (5 Punkte) Beispiele: *Main> updateMatrix exampleMatrix (2,0) Unmarked [ [Marked,Unmarked,Unmarked,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] *Main> updateMatrix exampleMatrix (0,0) (Open 10) [ [Open 10,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] c) eine Funktion neighbours :: GameState -> Coordinates -> [Coordinates], die für einen Spielzustand und Koordinaten, die Koordinaten aller benachbarten Zellen berechnet. Hierbei bietet es sich an, eine List Comprehension zu verwenden. (5 Punkte) Beispiele: *Main> neighbours exampleState (1,1) [(0,0),(0,1),(0,2),(1,0),(1,2),(2,0),(2,1),(2,2)] *Main> neighbours exampleState (0,3) [(0,2),(1,2),(1,3)] d) eine Funktion minesAround :: Coordinates -> GameState -> Int, die Koordinaten und einen Spielzustand erhält und berechnet, wieviele Minen sich in der Nachbarschaft der Zelle mit den gegebenen Koordinaten befinden. (5 Punkte) Beispiele: *Main> minesAround (3,0) exampleState 1 *Main> minesAround (0,2) exampleState 1 *Main> minesAround (3,2) exampleState 2 e) eine Funktion updateSingleCell :: GameState -> Coordinates -> GameState die einen Spielzustand und Koordinaten erhält und die Zelle mit den gegebenen Koordinaten sichtbar macht, d.h. wenn die Zelle mit einer Mine belegt ist, wird der Eintrag auf Mine geändert, und ansonsten wird der Eintrag auf Open i geändert, wobei i die Anzahl der Minen in der Nachbarschaft ist. (5 Punkte) Beispiele: *Main> updateSingleCell exampleState (0,0) GameState {matrix = [[Mine,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} *Main> updateSingleCell exampleState (1,3) GameState {matrix = [[Marked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Mine,Unmarked,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} 3 f) eine Funktion updateCells :: GameState -> Coordinates -> GameState, die einen Spielzustand und Koordinaten erhält und die Zelle mit den gegebenen Koordinaten sowie alle ihre Nachbarn sichtbar macht. Versuchen Sie die Funktion im wesentlichen durch updateSingleCell und foldl zu programmieren. (5 Punkte) Beispiele: *Main> updateCells exampleState (0,0) GameState {matrix = [[Mine,Mine,Open 2,Open 1] ,[Open 2,Open 3,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} *Main> updateCells exampleState (1,3) GameState {matrix = [[Marked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Open 1,Open 2,Open 3,Marked] ,[Open 1,Mine,Open 2,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} g) eine Funktion playStep :: Action -> GameState -> GameState, die eine Aktion und einen Spielzustand erhält und den nachfolgenden Spielzustand nach den oben beschrieben Regeln berechnet. Wenn der Gewinnzustand schon auf Verloren (Lost) oder Gewonnen (Won) steht, so geben Sie den Spielzustand unverändert zurück. (15 Punkte) Beispiele: *Main> playStep (Toggle (0,0)) exampleState GameState {matrix = [[Unmarked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} *Main> playStep (Toggle (0,3)) exampleState GameState {matrix = [[Marked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Marked,Unmarked,Unmarked,Unmarked]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Undecided} *Main> playStep (OpenEntry (3,3)) exampleState GameState {matrix = [[Marked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Unmarked,Unmarked,Unmarked,Marked] ,[Unmarked,Unmarked,Unmarked,Mine]] ,mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] ,state = Lost} *Main> playStep (OpenEntry (0,3)) exampleState GameState {matrix = [[Marked,Unmarked,Open 2,Open 1] ,[Unmarked,Unmarked,Mine,Open 1] ,[Open 1,Open 2,Unmarked,Marked] ,[Open 1,Mine,Unmarked,Unmarked]] , mines = [(0,0),(1,0),(2,1),(1,3),(3,3)] , state = Undecided} h) Auf der Webseite zur Veranstaltung finden Sie eine Haskell-Datei Main.hs. Diese verwendet die gloss-Bibliothek zum Anzeigen und zur Interaktion des Spieles. Die Datei importiert das Modul MineSweeper. Installieren Sie die gloss-Bibliothek3 und kompilieren Sie das Main-Modul4 . Führen Sie das Programm aus. Die möglichen Interaktionen sind: – linke Maustaste: Zelle aufdecken – rechte Maustaste: Markierung setzen/entfernen – Taste q: Spiel verlassen. 3 4 üblicherweise genügt es in einer Shell cabal install gloss einzugeben. Hinweise hierzu sind im Kopf der Datei Main.hs 4 (5 Punkte)