Lab 4 - Objektorienterad design och programmering

Uppgift

I den här labben ska ni skapa ett textbaserat äventyrsspel. Textbaserade äventyrsspel är spel där endast text och textkommandon används i spelet. Oftast ska man gå runt i en värld och hitta och använda saker man hittar i världen för att ta sig vidare. Ett känt svenskt textbaserat äventyrsspel är Stugan. Ifall ni vill testa ett sådant spel finns det ett spel i Emacs heter Dunnet, som ni kan starta med kommandot M-x dunnet i Emacs.

Ni ska skriva ett sådant spel som uppfyller specifikationen nedan. Det viktigaste i labben är dock er objektorienterade design, det vill säga vilka klasser och relationer dem emellan ni väljer att använda. Det är också viktigt att ni använder lämpliga datastrukturer (länkad lista, array eller hashtabell till exempel) i er kod.

Läs igenom hela beskrivningen noggrant innan ni sätter igång, så att ni är helt införstådda med uppgiften.

Specifikation

Ni ska skriva ett textbaserat äventyrsspel där man går runt i ett hus, och kan ta upp saker i huset, som man ibland kan använda till något. Huset ska ha ett antal rum, med dörrar emellan. Dörrarna ska kunna vara öppna, stängda och låsta. Man kan enbart gå mellan rum om det finns en öppen dörr mellan dem. Det ska även finnas föremål av olika typ i huset. En del föremål är fasta, och går inte att göra något med, en del kan man ta upp och bära runt på, och ytterligare andra kan man utföra handlingar med hjälp av, till exempel låsa upp en dörr med en nyckel. Spelet spelas med hjälp av enkla textkommandon, till exempel "gå N", för att gå till ett rum norrut, och "ta bok" för att ta en bok. Ett exempel på hur det skulle kunna se ut när man spelar finns här.

Beskrivning av världen

En spelvärld beskrivs i en textfil, där rum, dörrar, saker och startplats definieras. Ett exempel på en fil, som användes för spelexemplet, är:
//Rum
room Hall
room Kök
room Förråd
room Sovrum

//Dörrar
door N-S open Hall Sovrum 
door N-S closed Förråd Kök 
door Ö-V locked Hall Förråd
door Ö-V open Sovrum Kök

//Saker
thing låda Förråd CONSTANT
thing timglas Kök MOVE
thing nyckel Sovrum USE lås_upp

//Startplats
start Hall
som motsvarar följande värld

En specifikationsfil kan innehålla följande delar:

  • Specifikationen av rum startar med ordet "room" följt av ett rumsnamn, som kan vara ett godtyckligt namn, dock utan mellanslag.
  • Specifikationen av en dörr startar med ordet "door" och har tre delar:
    • riktning: "riktning1-riktning2", där riktningen åt ena hållet anges först, följt av bindestreck, och riktningen åt andra hållet
    • namnet på det första rummet dörren leder till
    • namnet på det andra rummet dörren leder till

    Riktningsangivelserna kan innehålla vilken sträng som helst, jag har valt att använda väderstreck. Ni får själva välja hur ni vill ange riktningar. Väderstreck brukar dock vara enkelt. Även höger/vänster/framåt el. liknande är möjligt, men svårare, då man bör hålla ordning på spelarens riktning i så fall. Vill man ha flera dörrar åt tex norr i ett rum kan man lösa det genom att ha riktningsangivelser som "N1-S", "N2-S". Vill man göra ett hus som man kan gå vilse i kan man ha dörrar där riktningarna inte stämmer, som "N-Ö".
  • Vilket rum man startar i anges med ordet "start" följt av ett rumsnamn (som måste vara ett av de rummen man har definierat)
  • Föremål anges med ordet "thing" följt av tre eller flera argument beroende på föremålstyp:
    • Namnet på föremålet (godtycklig sträng)
    • Namnet på det rum föremålet befinner sig i
    • Vilken typ av föremål det är:
      • CONSTANT: ett fast föremål som inte går att flytta
      • MOVE: ett föremål som man kan ta upp, men som inte går att använda
      • USE: ett föremål som går att ta upp, och som man kan använda
    • Om föremålet är av typen USE, anges sedan den eller de handlingar som kan utföras om man håller föremålet, för exemplet ovan kan man låsa upp en dörr, dvs använda kommandot lås_upp, enbart om man håller en nyckel. Det räcker att man kan utföra en handling per USE-objekt
  • Tomma rader och rader som börjar med "//" (kommentarer) ska ignoreras

Minst ett rum samt ett giltigt startrum måste alltid anges. Detta ska kontrolleras i er kod. Ni kan förutsätta att rumsnamn alltid ges först, innan de används i de andra kommandona. Uppfylls inte det behöver spelet inte klara av att läsa filen (det ska dock inte krascha, utan ge ett rimligt felmeddelande). Dessa möjligheter är basen för ert spel. Beroende på vad ni väljer att ha med i ert spel kan ni behöva utöka definitionerna något.

Som hjälp för att läsa in dessa filer, får ni en kodskiss nedan under "Hjälp och tips", som ni kan utgå ifrån.

Rumsfilen ska anges som argument när spelet startas, till exempel:

	java TextGame spelKonfiguration.txt
  

Kommandon

Ert spel ska innehålla följande kommandon:
  • gå DIR: Låter spelaren gå till rummet åt riktning DIR, ifall det finns en dörr ditåt som är öppen
  • ta SAK: Låter spelaren ta SAK om den finns i rummet, och inte är en fast (CONSTANT) sak. Saken finns då inte längre kvar i rummet, utan hålls av spelaren.
  • släpp SAK: Låter spelaren släppa SAK, om han håller den. Saken hamnar då i rummet där spelaren står
  • öppna DIR: Låter spelaren öppna en dörr i riktning DIR, om det finns en sådan dörr (och om den är stängd)
  • lås_upp DIR: Låter spelaren låsa upp en dörr i riktning DIR, om det finns en sådan dörr, och om man har en nyckel (som är definierad som att den ska klara handlingen lås_upp, se exempelfilen ovan, eller möjligen något annat föremål, som en dyrk, som är definierat med lås_upp)
  • visa: Beskriver rummet man är i
  • kommandon: Listar alla kommandon
  • håller: Listar alla saker spelaren håller i
  • sluta: avslutar spelet
för att underlätta kan ni låta alla kommandon anges som en token genom att använda understreck '_' i de fall där kommandot har flera ord, som "lås_upp".

Utöver dessa kommandon ska ni lägga till minst tre egna kommandon som använder föremål i spelet på samma sätt som lås_upp kräver att man har en nyckel, eller som påverkar dörrar. Några exempel på vad man kan tänka sig ges nedan. Vill ni kan ni välja tre av nedanstående förslag, men ni får gärna hitta på egna alternativ.

  • Hugg (med en yxa eller liknande) - Kan vara ett alternativt sätt att öppna dörrar, genom att hugga upp dem, man kan även tänka sig att man kan hugga sönder andra föremål, som då blir oanvändbara/går sönder.
  • Läs SAK (tex bok/tidning) - Skriver ut någon text som finns i boken, tidningen eller liknande. Texten kan vara relevant för spelet, en ledtråd av något slag, eller bara nonsens. Om ni gör detta kan det vara bra att utöka föremålsbeskrivningen i initieringsfilen, så att ni kan ange innehållet i läsbara föremål där.
  • Drick/ät mat/dryck - ger antagligen ingen direkt effekt för spelet. Men man skulle kunna tänka sig att man bara orkar göra vissa saker om man nyligen ätit/druckit (men då blir det svårare att implementera)
  • Öppna SAK - man skulle kunna ha föremål som innehåller andra föremål. Då måste man utöka föremålsbeskrivningen i filen för att klara av det. När man öppnar ett föremål skulle då det som ligger i det bli synligt. Till exempel skulle man kunna tänka sig att lådan i exemplet ovan går att öppna, och att den innehåller något annat. Detta tillägg kräver troligtvis att föremålsbeskrivningen uppdateras.
  • Undersök SAK - skulle kunna ge samma effekt som för öppna, att ett tidigare dolt föremål blir synligt. Man skulle även kunna tänka sig en effekt liknande den för läs, där en ledtråd kommer fram.
  • Ange lösenord - man skulle kunna ha dörrar låsta med kodlås istället för nyckel, där man måste ange ett lösenord för att öppna den (lösenordet skulle kunna förekomma som ledtråd någonstans). Om ni gör detta måste dörrbeskrivningen antagligen uppdateras.

Minst ett av era egna kommandon ska påverka speltillståndet på något sätt, det vill säga ändra hur världen ser ut, genom att till exempel ändra status på en dörr, på något föremål, eller göra ett tidigare dolt föremål synligt. Detta gäller (i normalfallet) inte för till exempel kommandona läs och ät/drick, som bara skriver ut en text.

Det går även att tänka sig andra möjligheter att utöka spelet än att lägga till kommandon enligt ovan. Till exempel skulle man kunna tänka sig att man vill kunna spara en spelomgång, så man inte alltid behöver börja om från början. Ifall ni hellre vill göra något annat än ett nytt kommando, så stäm av med Sara om det är ett rimligt alternativ innan ni sätter igång.

Spelinteraktion

Gränssnittet för spelet är textbaserat. Spelet skriver ut information om hur det ser ut i spelvärlden, och spelaren skriver in kommandon, enligt ovan. Se även exemplet på hur det kan se ut.

När spelet startas ska det skriva ut beskrivningen för det rum man är i när det startar. Spelet ska sedan skriva ut feedback baserat på de kommandon som körs. Vissa kommandon är förfrågningar efter information, som "visa" och "kommandon", och då ska denna information skrivas ut. För övriga kommandon ska antingen en bekräftelse på kommandot skrivas ut, eller ett felmeddelande, om kommandot var omöjligt att utföra. För kommandot "gå" ska rumsbeskrivningen för det nya rummet skrivas ut, ifall man lyckas byta rum. Om ett okänt kommando ges ska spelet skriva att det inte känner till det kommandot, eller liknande.

Rumsbeskrivningar kan vara enkla, som i exemplet, där bara namnet på rummet, vilka dörrar som finns, och vilka föremål som finns i rummet anges.

Felmeddelanden ska vara tydliga, och ge en anledning till att något gick fel. Det är inte godkänt att bara skriva ut "fel" eller liknande. Några exempel på felmeddelanden visas längst ner i spelexemplet. Om spelaren skriver något som är fel ska det alltid ges ett felmeddelande, spelet får inte ignorera det eller krascha.

Spelbeskrivningarna från spelet kan vara kortfattade. För rum och föremål kan namnen som anges i konfigurationsfilen användas. Ni behöver inte använda er av korrekta former på ord, det är till exempel helt OK att skriva "jag tog bok" om föremålet "bok" är definierat, istället för det korrekta "jag tog boken". Det samma gäller verbformer. Det är OK att skriva saker i stil med "Handlingen öppna utfördes på dörr N" eller "jag öppna dörr N", istället för mer flytande satser som "jag öppnade dörren som leder åt N".

Objektorientering och datastrukturer

Huvudsyftet med den här labben är att öva på objektorienterad modellering och på val av datastrukturer för en större programmeringsuppgift, där ni själva ska skriva all kod. Dessutom får ni öva på Javaprogrammering i allmänhet. Ni ska alltså tillämpa de delar ni lärt er i kursen hittills.

När det gäller den objektorienterade designen gäller det att noggrant fundera över vilka klasser ni ska ha och hur de ska hänga ihop. Ett extra krav på ert spel är att arv och polymorfism ska förekomma i minst ett fall. Som tumregler kan ni tänka på att varje klass ska ha en tydlig uppgift, och att försöka ha få beroenden mellan klasser, så att inte alla klasser känner till alla.

Utöver koden ska ni även lämna in ett översiktligt UML-klassdiagram över er kod. Alla klasser och relationer dem emellan ska vara med, men ni behöver inte skriva ut metoder och variabler för era klasser. Det är rekommenderat att jobba parallellt och iterativt med diagrammet och koden. Lämpligtvis skissar ni ett diagram innan ni börjar koda, som ni sedan uppdaterar efter hand ni kodar, och upptäcker att det kanske inte blir så bra att göra som ni tänkt er. Kontrollera noga i slutet att diagrammet verkligen stämmer med den slutliga koden.

Ni ska även använda er av inkapsling i er kod, det vill säga, ha variabler på så låg åtkomstnivå som möjligt, för att låta varje klass kontrollera sina egna variabler. I normalfallet bör alla instansvariabler vara privata, och endast kommas åt via metoder. För klasser med arv kan man även tänka sig protected. Ingen användning av publika variabler är godkänd, ifall ni inte har någon mycket god motivering.

När det gäller datastrukturer som arrayer, listor och hashtabeller, så använd Javas standardklasser för dem, till exempel ArrayList, LinkedList och HashMap. Fundera noga på vilken datastruktur som är lämpad för varje uppgift, utifrån er teoretiska kunskap om datastruktererna.

Utöver koden och UML-diagrammet ska ni även skriva en kort rapport (1-2 A4-sidor). I rapporten ska ni kortfattat beskriva de kommandon som ni själva lagt till i spelet. Men framförallt ska ni diskutera er objektdesignade design. Ni kan till exempel ta upp följande frågor. Var det lätt att lösa alla problem som uppstod med er design, eller var det något som blev krångligt som kunde blivit lättare om ni hade gjort på något annat sätt? Är det något val ni gjort som inte blev så lyckat, som ni skulle vilja ändra för att få en bättre design? Var det något i er ursprungliga design som ni var tvungna att ändra av någon anledning när ni väl kom igång med kodningen? Är det något särskilt i er design som ni är särskilt nöjda med? Ett exempel på något som kan vara antingen väldigt lätt eller väldigt krångligt att hantera beroende på hur ni gjort er design är att när man öppnar/låser upp en dörr, så måste den bli öppnad/upplåst från både hållen, inte bara från det rummet där spelaren står. Ta även upp era val av datastrukturer. Välj 2-3 exempel på val ni gjorde och diskutera för- och nackdelar med det valet. Hur valde ni till exempel att hålla ordning på alla rummen, i en lista, array eller hashmap, och varför?

Krav på ert spel

Följande krav ställs på ert spel:
  • Spelet ska fungera som det beskrivits ovan
  • Spelvärldens omfattning (ni kan skapa en helt egen spelvärld eller utgå från exempelvärlden)
    • Ert spel ska ha minst 6 rum
    • Det ska finnas dörrar som är öppna, stängda, och låsta
    • Det ska finnas minst ett föremål av varje typ CONSTANT och MOVE, samt minst fyra av typen USE (med handlingarna lås_upp och era egendefinierade handlingar)
  • Kommandon
    • Alla kommandon som beskrivits ovan ska finnas och fungera, samt tre egendefinierade kommandon
  • Stabilitet
    • Spelet ska aldrig krascha, utan ge felmeddelanden om något felaktigt kommando anges, eller en felaktig/icke-existerande spelfil anges
    • Felmeddelanden ska vara tydliga
  • Spelet ska ha en god objektorienterad design, och välvalda datastrukturer, se ovan
  • Arv och polymorfism ska förekomma
  • Inkapsling ska användas

Hjälp och tips

Jobba iterativt

Försök att inte skriv all kod på en gång, och sedan börja testa, då det tenderar att vara väldigt svårt att felsöka. Skriv istället en liten del av världen i taget, och kompilera och testa efter hand. Börja till exempel med en liten värld med bara ett rum, och läs in och skriv ut det och se att det stämmer. Fortsätt sedan till exempel med att lägga något mer rum med en öppen dörr, och testa att det fungerar. Eller lägg till en sak av varje typ i taget, och se att det fungerar som det ska. Även när det gäller kommandon är det lämpligt att lägga till ett i taget, och testa det.

Spårutskrifter

Ibland kan det vara svårt att verkligen veta att det ni tror händer i er kod verkligen händer. Ett sätt att hålla koll på det är att använda spårutksrifter, det vill säga skriva ut en utskrift när något särskilt händer eller om ni vill kontrollera värdet på någon variabel. Det är lämpligt att skriva sådana utskrifter till standard error, System.err, som är en ström där man kan skriva felutskrifter. För att göra det skriver ni till exempel:

	System.err.println("I funktionen X, lägger till " + thing);
  
I normala fall kommer dessa utskrifter att komma blandat med era vanliga utskrifter till System.out i terminalen. Men det går att omdirigera dem, så att de försvinner, genom att starta spelet såhär:
	java TextGame gamefile.txt 2> /dev/null
  
Gör ni så kommer enbart era vanliga utskrifter att visas, vilket gör att ni lätt kan växla mellan att visa bara spelutskrifter och även spårutksrifter Att använda System.err, gör det också enkelt att hitta alla spårutskrifter innan ni lämnar in, så att ni kan ta bort eller kommentera bort dem.
Använd Eclipse

Om du inte redan har gjort det, så rekommenderas att använda Eclipse till den här uppgiften. Eclipse är en IDE, Integrated development environment, en miljö för att koda Java (och andra språk) i, som erbjuder en hel del stöd i kodningsprocessen. Det är speciellt användbart att använda Eclipse när ni kodar ett lite större program med många klasser.

Eclipse kompilerar automatiskt koden medan du skriver, så inget separat kompileringssteg behövs. Du kör sedan koden inifrån Eclipse (glöm inte skriva in din spelkonfigureringsfil som argument). Eclipse kan hjälpa till med många saker, som att lägga till javadoc-kommentarsskelett för nya klasser och metoder, automatiskt generera kod, som konstruktorer och get- och setmetoder, åtgärda enkla kompileringsfel som saknade semikolon eller bortglömd import, mm.

Om du inte använt Eclipse tidigare kan det vara en viss inlärningströskel, men det finns många tutorials på nätet, se några förslag nedan. Fråga gärna din lärare om hjälp också!

Inläsning av spelbeskrivningsfilen
Som lite hjälp får ni här en kodskiss för hur inläsning av spelfilen kan gå till, som ni kan utgå ifrån. Den baseras på en viss uppsättning klasser, som ni dock kan behöva modifiera för att passa er.
// Skiss av funktion för att läsa in en spelfil. Ligger i mitt fall i en klass som heter Reader
// argumentet "in" är en Scanner som har skapats kopplad till spelbeskrvingsfilen
// man kan även tänka sig att få in filnamnet direkt hit och öppna en Scanner här
// GameWorld är en klass som innehåller alla entiter i spelvärlden
// metoden returnerar en boolean som talar om ifall inläsningen är lyckad eller inte

public boolean readWorld(Scanner in, GameWorld world) {

   int lineNo = 0;
   
   while (in.hasNextLine()) {
     String line = in.nextLine();
     lineNo++;

      String[] tokens = line.split("\\s+");
      if (tokens.length == 0 || tokens[0].startsWith("\s*//")) {
         continue; // kommentar eller tom rad ska ignoreras
      }

      try {
        String type = tokens[0];
   
        if (type.equals("room")) {
           readRoom(tokens, world);
        } else if (type.equals("door")) {
           readDoor(tokens, world);
        }
        //fler fall för "start" och "thing"
        else {
           //Se till att ge felmeddelande om felaktig configfil
        }
      }
      catch (Exception e) { //Eller fånga hellre mer specifika undantag separat,
                            //för att ge bättre felmeddelanden
         System.out.println("Problem när  spelbeskrivningsfilen lästes in\n" + e);
         System.out.println("Problemet uppstod på rad " + lineNo + ": " + line);
         return false;
         // används sedan som kontroll där den här funktionen anropas
         // för att kontrollera att världen skapats OK
      }
   }
   return true;
}


public void readRoom(String[] tokens, World world) throws TextGameReadingException {
   System.err.println("reading room");
   if (tokens.length < 2) { 
      throw new TextGameReadingException("ge ett rumsnamn som argument till room");
      //förutsätter att du skapat en Exception-klass TextGameReadingException
   }
   world.addRoom(tokens[1]);
}


public void readDoor(String[] tokens, World world) throws TextGameReadingException {
   System.err.println("reading door");
   if (tokens.length < 5) {
      throw new TextGameReadingException("ge 4 argument till door: dir-pair status room1 room2");
      //förutsätter att du skapat en Exception-klass TextGameReadingException
   }
   world.addDoor(tokens[1], tokens[2], tokens[3], tokens[4]);
}
I det här fallet finns metoderna "addRoom" och "addDoor" i klassen GameWorld, och lägger till dessa till spelvärlden. I "addDoor" kontrolleras även att båda rummen som dörren leder till faktiskt finns.

Notera att exceptions använts här för att sköta felhanteringen. Det är en variant som passar bra här, och som rekommenderas. Som står i en kommentar skickas ett boolean-värde tillbaka, som sedan används som test på att världen skapats OK efter anropet till denna metod, vilket är en annan typ av felhantering. Ytterligare en variant är att använda sig av null-värden för variabler som ej skapas, som man sedan kan testa för.

Redovisning

För den här labben ska följande delar lämnas in:
  • All er kod
  • Utskriften från en körning, där era nya kommandon ingår
  • Spelbeskrivningsfilen
  • UML-diagram kod för ert spel - ni kan välja att rita det för hand, och lämna in på papper, eller att rita det i något program. Det ska dock i så fall lämnas in i något vanligt format, som pdf eller png, inget specialformat
  • En kort rapport som beskriver era speltillägg, och diskuterar er objektorienterade design och val av datastrukturer

Checklista för inlämnat material:

  • Är alla delar inlämnade?
  • Har en god objektorienterad design använts?
  • Har ni använt lämpliga datastrukturer?
  • Används lämpliga och tydliga namn för klasser, metoder och variabler?
  • Följer koden kodkonventionerna?
  • Är koden enhetligt indenterad?
  • Är koden kommenterad enligt kodkonventionen?
  • Används en rimlig åtkomstnivå för klasser, variabler och metoder?
  • Används inkapsling?
  • Uppfyller programmet kravspecifikationen?
  • Stämmer er kod överens med ert UML-diagram?



Skapad för 5LN446, 2013 av Sara Stymne