Uppgift 5 - Sokoban

Bakgrund

I den här uppgiften implementerar vi, med hjälp av Scalas Swing-klassbibliotek, ett spel som heter Sokoban (känt också med namnet Boxxle). I spelet rör man sig i en vinkelrät labyrint, sett från ett fågelperspektiv, och skuffar omkring lådor med avsikten att antingen placera alla lådor på sina rätta ställen eller (som är fallet i vårt spel) att ta spelaren till en målruta.

Det finns ganska många deluppgifter, men bortsett från deluppgift 3 är de ganska korta. Det lönar sig att läsa varje deluppgift i sin helhet före man börjar med implementationen. Kom också ihåg att kompilera och testa programmet efter att du tillsatt nya funktioner.

Uppgiftsbeskrivning

Efter den här uppgiften vet du hur man:

Du kan ladda ner alla klasser som används i denna uppgift i det här paketet: luokat.zip.

1. Deluppgiften - Ramen Sokoban

Vi börjar med att definiera ett objekt med namnet Sokoban som ärver av SimpleSwingApplication. Det lättaste sättet att skapa ett simpelt program med ett grafiskt användargränssnitt är att ärva av SimpleSwingApplication-klassen. Genom att implementera top-metoden kan vi skapa ett huvudfönster åt programmet. top-metodens implementation skall vara sådan, att den returnerar ett MainFrame-objekt. Du kan se hur det görs i View.scala-filen som du använde i omgång 3.

I top-metoden skall du dessutom:

Till slut skapar vi ännu en menypanel åt fönstret. Menypaneler som är konstruerade med Swing består av tre olika typers objekt. Den första, MenuBar, fungerar som en container åt alla menyer, som representeras av Menu-objekt. De här Menu-objekten kan innehålla flera kommandon, som representeras av MenuItem-objekt. Skapa alltså en menypanel åt ditt fönster, dit du tillsätter en meny som heter "Spelet", och in i menyn två kommandon som heter "Börja om" och "Sluta". I senare deluppgifter länkar vi funktioner till kommandona.

I det här skedet har du redan:

Kom ihåg att försöka köra ditt program!

2. Deluppgiften - Enumerationen GridType

I den här deluppgiften bekantar vi oss med en speciell klass — enumerationen. En enumeration är en uppräcknad datatyp, en slags lista av alla element av någon specifik grupp. Man kan t.ex. skapa en enumeration som reflekterar veckodagarna genom att inkludera alla veckodagar (måndag, tisdag, onsdag, torsdag, fredag, lördag och söndag) som element samt möjligtvis metoder som man kan använda dem med. I en enumeration definierar man alltså på förhand alla möjliga instanser av klassen. Detta kan användas då man har en begränsad mängd möjliga alternativ och man vet alternativen på förhand.

Implementera en klass, GridType, som beskriver rutorna på spelplanet genom att ärva från Enumeration-klassen. Skriv din enumeration i en separat fil. Du får hjälp med att definiera typerna genom att läsa dokumentationen av Enumeration. Typerna som du behöver i den här uppgiften är:

I det här skedet skapar vi en hjälpmetod åt vår enumeration, som låter oss färgsätta spelplanet genom att associera varje värde i enumerationen med en färg. Skriv alltså en metod getColor, som tar emot ett värde av enumerationen GridType, och som returnerar ett Color-objekt. Det lättaste och bästa sättet att åstadkomma detta är genom att använda match-strukturen. Du kan själv välja färgerna för rutorna på spelplanet.

Efter den här deluppgiften har du:

3. Deluppgiften - Spellogiken

Nu skall vi bygga logikmaskineriet, som utgör grunden för spelet och som upprätthåller spelplanets tillstånd, dvs. väggarnas och de flyttbara hindrens plats samt spelarens och målets läge. För spellogiken skapar vi en egen klass, som inte är beroende av något visst användargränssnitt, utan som med ett passligt gränssnitt (dvs. med kompatibla metoder) skulle kunna användas med många olika grafiska- och icke-grafiska gränssnitt. I princip skulle du alltså kunna ta en spellogikklass av din vän och den skulle fungera ihop med din grafiska implementation. Det är vanligtvis (om det inte är frågan om ett väldigt simpelt program) en bra idé att separera programlogiken och användargränssnittet från varandra — då kan man göra ändringar i den ena ändan utan att behöva anpassa till dem i den andra ändan.

Spelets logiska kärna är en klass med namnet Game. Den ärver endast av Object (dvs. den är inte en subklass av någon av Swing bibliotekets klasser). Klassen representerar spelplanet som en tvådimensionell tabell vars element är instanser av den nyss skapade enumerationsklassen GridType.

Klassens konstruktor tar som parameter en fil (java.io.File) på basen av vilken den skapar spelplanet. Filen bör innehålla speciellt formulerade textrader, som läses in med hjälp av java.util.Scanner-klassen och som sedan tyds och översätts till ett spelplan. Ett exempel på ett simpelt spelplan kan du hitta i filen kentta1.txt. Filerna skall följa följande format:

Ladda ner som underlag åt din klass: Game.scala.

try-catch epäonnistuu
Kom ihåg att fånga alla undantag som behöver handlas i catch-blocket. I bilden representerar hundbenet det för alla kända undantaget NullPointerException.

Märk, att det används ett konstigt try-catch -trick i början av klassens implementation. try-catch-finally är en struktur som används i Scala och som man kan hantera undantag (alltså instanser av klassen Exception, som du också kommer att få använda snart) med. try-blocket innehåller kod som kan kasta undantag, medan dessa möjliga undantag kan fångas och hanteras i catch-blocket.

I catch-blocket kan man hantera en eller flera olika typer av undantag. Märk dock att om den första och/eller enda undantagstypen är Exception, kommer alla undantag att fastna i blocket. catch-blockets interna struktur baserar sig på fall som är bestämda av case-satser (på samma sätt som i match-strukturen). Man kan utöver de två redan nämnda blocken använda ett finally-block, vilket exekveras oavsett om ett undantag uppstod eller inte. Du kan läsa en noggrannare förklaring med exempel här: http://www.tutorialspoint.com/scala/scala_exception_handling.htm.

När du skriver dina egna try-catch -block skall du komma ihåg att inkludera all sådan kod, som beror på resultatet av något som kan kasta ett undantag, i samma try-block så att det inte går som för hunden i bilden ovan. Det lönar sig inte alltid heller att försöka vara programmerarnas Ash Ketchum och försöka ta fast alla undantag. Det finns fall då programmets exekvering bör sluta vid ett undantag.

Och sedan flyttar vi oss till lite mera seriösa saker, dvs. implementationen av denna deluppgift.

Du bör implementera åtminstone följande metoder i klassen Game:

När din Game-klass är klar kan du testa dess funktionalitet med följande testklass: SikobanTest.scala. Den använder rutnätet i ett simpelt märkesbaserat Sokobanspel, som kan spelas genom att alltid ge rutans koordinater som man vill att spelaren skall flytta sig till näst.

Det börjar bli dags att knyta spellogiken som du skapat ihop med det grafiska användargränssnittet, som du tidigare började på.

Efter den här deluppgiften har du:

4. Deluppgiften - Klassen GamePanel

Nu skapar vi en panel, som representerar ett spelplan av rutor. Klassen GamePanel skall ärva av GridPanel. På grund av att GridPanel-klassens konstruktor vill veta antalet på raderna och kolumnerna i panelen måste vi definiera en liknande konstruktor även åt vår GamePanel-klass. Utöver de nyss nämnda parametrarna tar vår konstruktor även en instans av den nyss skapade Game-klassen.

I det här skedet kan du snabbt återvända till Sokoban-klassen och integrera GamePanel-klassen som en del av användargränssnittet. Placera GamePanel i den tidigare skapade BorderPanel-containern. Lägg till ett try-catch-block, på samma sätt som i Game-klassen, där du försöker börja ett nytt spel med den givna kentta1.txt-filen. Här kan man bara reagera på undantag på ett sätt: Printa ut felets meddelande.

Vi använder Graphics2D-objektet, som introducerades i ett PBL-fall, till att rita rutnätet i GamePanel-klassen. Vi överlagrar den skyddade (protected, finns inte i Scala API:n pga. synligheten) metoden paintComponent, som tar som parameter Graphics2D-objektet. Metoden skall inte returnera något. Scalas Swing ser till att paintComponent-metoden kallas automatiskt alltid då dess förra invokering har avslutat.

Vår implementation av paintComponent-metoden fungerar så att varje ruta i Game-instansen ritas med en färg som bestäms av GridType-enumerationen. Sätt storleken av rutorna till (30, 30) pixlar och använd Graphics2D-objektet på samma sätt som i PBL-fallet.

Därefter implementerar vi interaktionen mellan spelaren och spelplanet. Du kan själv välja om du vill att man ska spela genom att flytta på musen eller genom att klicka på planet (Lyssnandet av tangentbordet lämnas till en bonusuppgift). Gör alltså så att GamePanel lyssnar på de valda funktionerna av musen genom att använda listenTo-metoden.

Definiera sedan reaktionerna till musens händelser (antingen MouseMoved eller MouseClicked) genom att modifiera GamePanel-klassens reactions-fält. Från händelsen som musen orsakat kan du få reda på var musens position var då händelsen genererades: Med hjälp av den kan du sedan be Game-instansen att flytta spelaren till den motsvarande rutan. Implementera lyssnandet av musen så att det inte längre går att röra spelaren efter att spelet har blivit slutfört.

Pröva också grafiskt att spelaren rör sig ordentligt.

Efter den här uppgiften har du:

5. Deluppgiften - Spelets status

I den här deluppgiften implementerar vi en mekanism som visar information om spelets ställning. Skapa ett Label-objekt, som du modifierar i början av spelet så att den indikerar att spelet är igång. Kom ihåg att ansluta den som en del av användargränssnittet, under spelplanet.

Skapa också en offentlig metod, som tar som parameter en sträng, och som ändrar Labelns text till den givna strängen.

Nu kan du ändra GamePanel-klassens reaktion till att spelet slutar med att ändra Labelns text.

Efter den här uppgiften har du:

6. Deluppgiften - Lyssnandet av kommandona

I den här deluppgiften skapar vi lyssnare åt kommandona i menyn som du implementerade i den första deluppgiften.

Sluta-kommandot

Modifiera skapandet av kommandot så att det tar ett nytt Action-objekt som parameter. Det här objektet tar som parameter namnet på kommandot. Bygg ett klassblock åt denna instans där du implementerar apply-metoden och sätter en genvägstangentkombination åt kommandot. Här är lite hjälpmedel som får dig att komma igång:

contents += new MenuItem(new Action("valikon nimi") {
  def apply() { /* apply metodin sisältö */ }
  accelerator = Some( /* näppäinkomento, johon valikko reagoi */ )
})
Vi definierar alltså en ny, anonym klass som vi använder enbart här. Modifiera apply-metoden så att den slutar programmet. Då du väljer genvägstangentkombinationen får du använda eget omdöme, men ett vanligt val i det här fallet kunde vara t.ex. alt+F4. Du kommer att behöva Java Swing -klassbibliotekets KeyStroke-klass för att implementera genvägarna. Det lättaste sättet är att använda konstruktorn som tar en sträng som parameter.

Börja om -kommandot

Att börja om spelet implementeras på samma sätt som i föregående fall, men förstås med en annan implementation av apply-metoden och med en annan genvägstangentkombination. apply-metoden borde uppdatera texten i fönstrets undre del så att den visar att spelet har börjats om och också se till att spelplanet återställs till sitt börjantillstånd. Använd här startOver-metoden som du implementerade i spellogikklassen Game.

Du kan igen själv välja vad för tangentkombination du vill använda som genväg, men ett typiskt val är F2-knappen.

Efter den här uppgiften har du:

7. Deluppgiften - Förslag till bonusuppgifter

I det här skedet borde du ha ett fungerande Sokoban-spel som fungerar som sådant.. Den är dock kanske inte så mångsidig eller snygg. Du kan utveckla ditt spel vidare med några bonusuppgifter (krävs inte för fulla poäng), varav några introduceras i denna deluppgift. Du kan också alltid utveckla dina egna idéer.

Snyggare grafik

I luokat.zip-paketet som du laddade ner fanns det några bilder, som har tillsatts med tanke på spelets utseende. Du kan använda dessa bilder, eller skapa dina egna, för att få mera liv i spelet. Du kan använda bilderna för att representera spelaren (svinet) och hinder (tjocka svinet).

Du kan använda samma Graphics2D-objekt som du använde i paintComponent-metoden för att rita bilder på rutan.

Byte av spelplan

Lägg till i menyn ett nytt kommando som byter filen för spelplanet. Implementera en lyssnare åt kommandot på samma sätt som i 6. deluppgiften. Du skall implementera en filväljare (FileChooser), som accepterar textfiler. Du kan begränsa filtyperna med hjälp av FileFilter-klassen.

Skapa en ny metod i Game-klassen, som överlagrar startOver-metoden genom att ta som parameter ett File-objekt, som sedan används för att börja ett nytt spel.

Generering av spelplan (Svår: värd 6p)

Skapa en mekanism till spelet där det genereras ett nytt spelplan (som går att vinna) för varje spelomgång.

8. Deluppgiften - Till slut

Testa ännu alla klasser du implementerat och se till att allt fungerar så som du vill. Kommentera senast i det här skedet de delar av din kod som kan vara svårförståeliga. Packa hela lösningen så att assistenten inte behöver ladda ner något extra för att få programmet att fungera. Inkludera alltså i paketet som du returnerar åtminstone följande filer: Game.scala, GamePanel.scala, GridType.scala, SikobanTest.scala, Sokoban.scala, kentta1.txt och eventuella bildfiler som du kanske använt. Inkludera ännu readme.txt, vars underlag du kan kopiera nedan:

# ME-2120 Höst 2013
# 
# Omgång 5: Sokoban

Studentnumret:
Tid i timmar som har använts till de obligatoriska uppgifterna (uppskattning):
Tid i timmar som har använts till bonusuppgifterna (uppskattning):

# Märk de delar av omgången som du har implementerat med ordet 'gjort'
# Du kan också märka de delar som du har börjat på, men inte slutfört med ordet 'försökt'
# Du kan också nämna varför du inte lyckades med att slutföra dessa delar.

D1 Ramen Sokoban:                               inte gjort
    Fönstret skapas och den har rätt storlek:   inte gjort
    Fönstret öppnas i mitten av rutan:          inte gjort
    Fönstret har en menyrad:                    inte gjort
D2 Enumerationen GridType:                      inte gjort
    Enumerationen innehåller de krävda typerna: inte gjort
    Enumerationen har den krävda hjälpmetoden:  inte gjort
D3 Spellogiken:                                 inte gjort
    Spelplanet tyds rätt:                       inte gjort
    Spelaren rör sig enligt reglerna på planet: inte gjort
    Spelet kan ta slut:                         inte gjort
D4 Klassen GamePanel:                           inte gjort
    Spelplansfilen tyds grafiskt rätt:          inte gjort
    Spelet reagerar till användarens kommandon: inte gjort
D5 Spelets status:                              inte gjort
    Spelets status uppdateras vid behov:        inte gjort
D6 Lyssnandet av menykommandon:                 inte gjort
    Kommandot som avslutar spelet fungerar:     inte gjort
    Kommandot som börjar om spelet fungerar:    inte gjort
	
# Gjorde du (av de givna, eller egna) bonusuppgifter?
(Berätta vad du gjorde och eventuellt hur man använder dem ifall funktionaliteten skiljer sig avsevärt
från det som tidigare implementerats.)

# Blev det kvar fel, eller ställen som inte fungerar korrekt i ditt program?
(Lista alla problem som du känner till. Nämn, om möjligt, din egen uppskattning om deras orsaker och
vad du har försökt för att fixa dem.)

# Fria kommentarer gällande den här uppgiften?
(Du kan fritt kommentera uppgiften, men kom ihåg att ge den egentliga feedbacken via feedbackblanketten.)
Fyll i readme.txt och inkludera den i paketet som du returnerar. Namnge paketet enligt formen studentnr_kierros5.zip. (Och inga skämtare den här gången, tack — sätt ditt studentnummer istället för studentnr :).

Returnering

Returnera uppgiften till Rubyric-systemet senast lördagen den 23. november kl. 18.00. Om du försenar dig med returneringen så bestraffas du med 2 poäng för varje 24-timmars period som har hunnit börja efter deadlinen. Om du t.ex. returnerar ditt arbete på söndagen kl. 18.01, så bestraffas du med 2*2=4 poäng. Returnering av paket med fel filformat (de ända tillåtna formaten är .jar och .zip) bestraffas med ett poäng. Om en .scala-fil saknas bestraffas du med ett poäng, och om readme.txt-filen saknas bestraffas du också med ett poäng.

Efter returneringen skall du ännu fylla i feedbackblanketten här: http://www.cs.hut.fi/cgi-bin/teekysely.pl?action=showform&id=studio1-scala5-2013&lang=FIN

Bedömning

Programmeringsuppgifterna bedöms enligt de nedan givna kriterierna. Uppgiftens maximipoäng är 60 poäng och det krävs åtminstone 30 poäng för att få godkänt.

Om den returnerade uppgiften är tydligt halvfärdig och den får under den godkända mängden poäng, kan den returneras till studenten för förbättring (som en sk. bumerang). Uppgiften godkänns först då när den har blivit tillräckligt omfattande och djupgående. En bumerang kan godkännas högst med minimipoängmängden 30.

Det lönar sig att lägga märke till kodens indentering och klarhet; om assistenten kan läsa din tankegång tydligt från koden, hjälper det betydligt med uppskattningen av kodens funktionalitet och felens allvarlighet. Du bör också försäkra dig om att den returnerade koden verkligen kompilerar och att du returnerar den senaste versionen av din lösning.

Det är dessutom möjligt att för bonuspoäng för prestationer som överskrider uppgiftsbeskrivningen.