Projekt Labyrinter Java I, DT006G/DT134G, 2015-06-08, jimahl* Innehåll Introduktion... 2 Förutsättningar... 2 Mål... 2 Uppgift... 2 Labyrinter och träd... 2 Depth-first search... 5 Allmänna krav... 7 Betyg E... 7 Betyg D... 8 Betyg C... 8 Betyg B... 8 Betyg A... 9 Examination... 9 *Jimmy Åhlander: jimmy.ahlander@miun.se Baserad på Labyrintlaborerande (maze.pdf) för C++ av Martin Kjellqvist (2013-05-16) 1
Introduktion Labyrinter kommer i många olika former. I detta projekt kommer vi att skapa labyrinter som inte innehåller loopar. Dessa labyrinter har alltid en unik lösning från start till slut. De kan betraktas som träd (en typ av graf) där startpunkten är trädets rot och de olika vägarna är trädets grenar. Detta har en stor betydelse för de algoritmer som kommer att tillämpas i projektet. Förutsättningar Utrustning Litteratur Laborationer Java SDK eller JDK, JRE, NetBeans eller Eclipse Kurslitteratur och material på kurswebbplats. Lämpligen är samtliga laborationer inlämnade innan projektet påbörjas. Mål Studenten ska efter genomfört projekt kunna: Nyttja grundläggande algoritmiska koncept för problemlösning. Använda datastrukturer som ArrayList och Stack. Utforma program där lösningen uppdelats i enkla beståndsdelar. Skydda sig mot fel vid inmatning. Uppgift Din grunduppgift är att skapa ett program som kan generera labyrinter med hjälp av algoritmen Depth-First Search (DFS). För de olika betygsstegen krävs varierande ansatser läs därför igenom hela instruktionen innan du hugger in i koden. Labyrinter och träd Labyrinter kan representeras på flera olika sätt. Grundläggande är att labyrinter alltid består av gångar och väggar paths and walls. Vill vi expandera konceptet lite kan vi också tänka oss att det finns noder mellan gångarna mer om det snart. En labyrint som genererats med algoritmen DFS kommer aldrig att inneha några cykler d.v.s. det finns ingen väg som går runt-och-runt. Detta innebär också att det endast finns en unik väg från starten till slutet eller från vilken plats som helst i labyrinten till en annan plats, för den delen. En labyrint som genererats med DFS kommer också alltid att använda hela spelbrädet d.v.s. det kommer aldrig finnas någon del av spelbrädet som helt saknar gångar. En diskutabel nackdel med generell DFS är emellertid att labyrinterna har en tendens att bli relativt enkla att lösa. Då algoritmen föredrar djupdyk uppstår det färre förgreningar än när vissa andra algoritmer används. Låt oss kika på två labyrinter, se Figur 1 och Figur 2. Den röda rektangeln markerar startnoden. Övriga noder är markerade med en mörkgrön färg och gångarna mellan dem med en ljusgrön färg. Båda två saknar emellertid utgång. 2
Figur 1. En labyrint genererad med hjälp av DFS. Figur 2. En labyrint som innehåller en cykel och är inte heller fullständig då det övre högra hörnet inte är sammankopplat. Vi kan också representera dessa labyrinter med hjälp av grafer, se Figur 3 och Figur 4. Strecken markerar gångarna mellan noderna. Figur 3. En graf som representerar labyrinten från Figur 1. Grafen innehåller inga cykler och är därför ett träd. Figur 4. En graf som representerar labyrinten från Figur 2. 3
Att representera labyrinten i kod kan utföras på olika sätt. Om vi tänker oss att vi använder tecken för varje vägg, gång och nod så skulle vi kunna köra på en tvådimensionell array: char[][] maze; S.........................................E Alternativt, för samma typ av lösning, skulle vi kunna använda en mer funktionell ArrayList: ArrayList<ArrayList<Character>> maze = new ArrayList<>(); Det går också bra att skapa en egen klass för noderna istället för att spara tecken direkt. Då representeras gångarna som anslutningar mellan noderna, istället för som egna tecken i en lista eller array: ArrayList<ArrayList<MazeNode>> maze = new ArrayList<>(); public class MazeNode { private boolean connectedleft; private boolean connectedright; private boolean connectedup; private boolean connecteddown; private boolean visited; } Vid utskrift kommer labyrinterna likväl att se likadana ut det är åtminstone tanken. Valet för hur labyrinten ska representeras påverkar endast hur den sedan hanteras när DFS tillämpas, och när den ska skrivas ut. Ett hett tips är att ändra typsnittet för konsolen i din utvecklingsmiljö till något med fast teckenbredd, t.ex. Courier New eller Lucida Console. Annars kommer labyrinten troligen se lite lustig ut vid utskrift då olika tecken är olika breda i de flesta typsnitt. T.ex. ovanstående labyrint med Arial som teckensnitt istället för Courier New: S.........................................E 4
Depth-first search För att skapa en labyrint som både är slumpmässigt utformad och åtminstone har någon form av svårighetsgrad behöver vi en väl vald algoritm. En sådan algoritm är Depth-first search (DFS). DFS kan användas för olika syften, men när det handlar om att generera labyrinter kan den sammanfattas med följande steg: 1. Sätt startnoden som den aktiva noden A och markera den som besökt. 2. Så länge som det finns obesökta noder: 1. Om den aktiva noden har några obesökta grannar: 1. Lägg till den aktiva noden till stacken. 2. Välj slumpmässigt en av de obesökta grannarna 3. Ta bort väggen mellan den aktiva noden och den slumpmässigt valda noden. 4. Sätt den slumpmässigt valda noden som den aktiva noden och markera den som besökt. 2. Annars om stacken inte är tom 1. Poppa en nod från stacken och sätt den som aktiva noden. Vi introducerar här ett nytt begrepp: stack. En stack är en datastruktur som följer principen Last In, First Out (LIFO). Tänk dig att du staplar böcker på en fin hög (stack) där vi bara kan lägga till och ta bort böcker från högst upp på högen, se Figur 5. Så funkar en stack. Figur 5. En stack har funktionerna push och pop. Vi introducerar också en skillnad på besökta och obesökta noder. Detta kan vi implementera på valfritt sätt med hjälp av en enkel lista av typen boolean (true/false) för varje nod. Låt oss testa algoritmen på en liten 3x3-labyrint. Vi säger att vi har en Stack S för vägen vi utforskar, en Lista L över hittills besökta noder, en aktiv nod A, och att noder har index 1-9 som angivet i första bilden. 5
A: 1 S: L: 1 Vi sätter nod 1 som startnod (röd färg) och även som den aktiva noden. Den sätts dessutom som besökt i L. A: 1, 4 S: 1 L: 1, 4 1 har obesökta grannarna 2 och 4. Vi väljer slumpmässigt 4. 1 läggs till stacken. Vi tar bort väggen mellan noderna 1 och 4. Vi sätter slutligen 4 som den aktiva noden och markerar den som besökt. A: 1, 4, 5 S: 1, 4 L: 1, 4, 5 På samma sätt som i förra steget väljs nod 5 slumpmässigt ut. 4 läggs till stacken. Väggarna tas bort mellan 4 och 5. 5 sätts som den aktiva noden och markeras som besökt. A: 1, 4, 5, 8 S: 1, 4, 5 L: 1, 4, 5, 8 Åter igen väljer vi en slumpmässigt obesökt granne, denna gång till den aktiva noden 5. A: 1, 4, 5, 8, 7 S: 1, 4, 5, 8 L: 1, 4, 5, 8, 7 A: 1, 4, 5, 8, 7, 8 S: 1, 4, 5, 8 L: 1, 4, 5, 8, 7 Nu börjar det bli spännande. Vi har hamnat i en situation där det inte längre finns någon obesökt nod till den aktiva noden 7. Vi poppar då stacken (tar ut den senast inlagda noden) och sätter den som aktiv nod. A: 1, 4, 5, 8, 7, 8, 9 S: 1, 4, 5, 8, 8 L: 1, 4, 5, 8, 7, 9 Den aktiva noden är då alltså åter igen 8. Då den endast har en enda granne, 9, blir den slumpmässigt vald. A: 1, 4, 5, 8, 7, 8, 9, 6 S: 1, 4, 5, 8, 8, 9 L: 1, 4, 5, 8, 7, 9, 6 A: 1, 4, 5, 8, 7, 8, 9, 6, 3 S: 1, 4, 5, 8, 8, 9, 6 L: 1, 4, 5, 8, 7, 9, 6, 3 6
Efter det sista steget finns det inte längre några obesökta noder i L en enkel kontroll visar oss att alla nummer från 1-9 finns där. Algoritmen är därför klar. A: 1, 4, 5, 8, 7, 8, 9, 6, 3, 2 S: 1, 4, 5, 8, 8, 9, 6, 3 L: 1, 4, 5, 8, 7, 9, 6, 3, 2 Det är kanske inte uppenbart för en så pass liten labyrint, men stackens syfte var i detta sammanhang att hålla koll på vilken väg vi kom ifrån i de fall där vi behöver backa tillbaks för att det tog slut på obesökta grannar. Som en bonus visar stacken alltid också kortaste vägen från starten till den aktiva noden. Allmänna krav För projektet bedöms helheten av din lösning inte endast den funktionella delen. Samtliga nedanstående krav spelar in i betyget där misslyckande på enstaka punkter påverkar betyget negativt, och misslyckande på flertalet punkter kräver komplettering. Din lösning ska vara uppdelad i lämpliga beståndsdelar, d.v.s. klasser och metoder. Koden ska vara kommenterad till en rimlig nivå. Det innebär bl.a. att metoder ska vara kommenterade gällande parametrar, vad som returneras, och vad de har för syfte. Det innebär också att mer komplicerade delar av koden ska innehålla kommentarer. Hellre fler kommentarer än färre, men det innebär samtidigt inte att varje enstaka kodrad eller deklaration ska ha en kommentar. Lämpliga variabelnamn ska användas. Det innebär att camelcase tillämpas och att namnen är självförklarande (jämför x med dist med distancethrown). Konsekvent språk och typografi (bl.a. indentering, blanda ej svenska och engelska). Informativa utskrifter och enkel inmatning. Temporära variabler ska skapas så lokalt som möjligt. Ingen kodupprepning. Ingen hårdkodning. Givetvis ska koden även kompilera problemfritt, helst utan anmärkningar, och kunna köras utan att krascha. Betyg E Ditt program ska kunna generera slumpmässiga, fullständiga labyrinter med hjälp av algoritmen DFS. Din lösning måste använda sig av en stack. Det ska vara möjligt för användaren att mata in en storlek för labyrinten och också att skriva ut labyrinten på lämpligt sätt genom ett användargränssnitt. Du behöver inte använda grafiska komponenter, utan det går bra att representera labyrinten med tecken, t.ex.. för gångar och (ASCII, alt + 219) för väggar. Ingång och utgång kan på samma sätt representeras med valfritt tecken. Du kan sätta dessa på valfria positioner. Lämpligen implementerar du även en metod för att nollställa labyrinten så att det är möjligt att generera nya labyrinter utan att starta om programmet. 7
Betyg D Ditt program lever upp till alla krav ställda av tidigare betygsnivåer. Det ska inte vara möjligt att krascha ditt program genom dålig indata. Exempel inkluderar att försöka skapa en labyrint med negativ storlek eller utanför gränsen för använda datatyper, eller om det helt enkelt råkar slinka in någon oönskad bokstav. Se även upp för problem vid generering mycket små labyrinter, t.ex. av storleken 1x1. Felmeddelanden ska vid problem skrivas ut till System.err och vara så precisa som möjligt. Betyg C Ditt program lever upp till alla krav ställda av tidigare betygsnivåer. Genereringen av labyrinterna ska kunna presenteras, steg för steg, för användaren, om användaren så önskar. D.v.s. labyrinten ska kunna printas för varje steg algoritmen tar men endast för de steg där den kopplar ihop två noder, och ej när den backtrackar genom stacken (och labyrintens utformning är oförändrad). Det ska också vara möjligt för användaren att skriva ut den genererade labyrinten till en textfil, med valfritt tillvägagångssätt. Betyg B Ditt program lever upp till alla krav ställda av tidigare betygsnivåer. Genereringen av labyrinterna ska kunna viktas. Detta har betydelse för när den slumpvalda grannen väljs. Om vi exempelvis viktar det så att det är 50 % troligare att vi väljer en obesökt nod som ligger horisontellt sett till oss själva, eller alternativt vertikalt så kommer vi att skapa labyrinter som innehåller långa horisontella, respektive vertikala gångar. Fenomenet är tydligast för lite större labyrinter. Inkludera åtminstone tre nivåer (normal, lite viktad, mycket viktad) för att vikta genereringen åt varje riktning (horisontell, vertikal). Vill du istället att användaren själv ska få mata in en exakt procentsiffra för viktningen är det självklart också okej. Med 100 % horisontell viktning blir det en ganska slalomaktig historia: S...................E 8
Betyg A Ditt program lever upp till alla krav ställda av tidigare betygsnivåer. Ditt program ska, förutom att kunna generera labyrinter, även kunna lösa dem. DFS eller Breadth-first search (BFS) kan användas för detta syfte, på nästan samma sätt som när labyrinten genererades. Det finns endast en lösning till fullständiga labyrinter från start till slut vilket innebär att när slutet väl hittats har du även hittat den bästa lösningen. Startet och slutet placerar du efter eget tycke, eller slumpmässigt. Lösningen ska presenteras grafiskt utan redundanta noder, d.v.s. avstickare som algoritmen tar medan den letar efter utgången ska inte synas. Soooo... o.. ooo.... o.. ooo... o... ooo.... o...ooooooe Examination För studerande på campus Du redovisar projektets funktion och din bakomliggande kod muntligen vid ett förbestämt redovisningstillfälle. Om du har fått tillgång till ett granskningsprotokoll så skriver du ut detta och fyller i de relevanta fälten. Glöm inte bort att du under redovisningen måste kunna redogöra för att programmet lever upp till såväl de allmänna kraven som de funktionella kraven. Vid godkänd redovisning lämnar du in projektet i inlämningslådan på kurswebbplatsen. Ingen rapport krävs för detta projekt. För studerande på distans Bifoga ditt projekt till din inlämning. Om du skriver din kod utan IDE som NetBeans zippar du helt enkelt alla källkodsfiler. Ange i headern för dina filer, förutom ordinär information som namn och datum, även vilken betygsgrad du har siktat på sett till vilka funktionella krav du försökt att uppfylla. Du ska alltså inte gardera dig har du försökt att implementera hela vägen upp till A så skriver du A, även om du misstänker problem. Kom ihåg att kika igenom de allmänna kraven en sista gång innan inlämning då detta kan få en stor inverkan på slutbetyget. Ingen rapport krävs för detta projekt. 9