Göm menyn

Labb 4: Tetris

Syfte

Denna labb, som precis som de tidigare görs enskilt, har tre syften:

  1. Att du ska skriva ett komplett, fungerande program. Tidigare uppgifter var jämförelsevis små och enkla. Här bygger vi istället upp något konkret och sammanhängande under en längre tid. Vi gör detta i form av ett spel: Tetris.

  2. Att du ska testa GUI-programmering i Java. Tetris ger flera möjligheter att utforska GUI-programmering.

  3. Att utforska fler OO-begrepp och modelleringsfrågor. För att leda dig åt rätt håll när du omsätter teori i praktik är inledningen i "tutorial"-form. Mot slutet får du gradvis mer och mer frihet – och färre detaljerade ledtrådar.

  4. Vi kommer inte i denna labb att implementera funktioner som poängvisning, high score, eller omstart av spel. Om du vill att ditt spel skall vara mer komplett får du gärna lägga till detta själv i mån av tid, men tänk på att du också ska ha tid för projektet.

Förberedelser

Om du inte har spelat Tetris tidigare: Läs på lite om det och testa någon variant. Beskrivningen förutsätter att du vet hur Tetris fungerar.

Info: Varför just Tetris?

En fördel med Tetris är att det inte är så krävande i fråga om t.ex. animering, kollisionsdetektering och andra knepigheter som inte har så mycket med objektorientering att göra. Vi kan därför fokusera mer på OO.

Info: Att bryta ner ett projekt i delar

Så hur programmerar man ett lite större sammanhängande projekt såsom ett Tetris-spel? Ett bra tips är att bryta ner det i steg eller milstolpar som kan implementeras i tur och ordning – helst på ett sätt som gör att man kan testa varje steg för sig och känna att man faktiskt har åstadkommit något och kommit vidare. Hela labben är uppbyggd på detta sätt.

Info: Bra att tänka på

  1. Arbeta på egen tid! Denna labb är betydligt större och många kommer att behöva betydligt mer tid än det som schemalagts.

  2. Använd gärna referenskortet för IDEA när du programmerar. Många funktioner kan vara mycket användbara här och i projektet.

  3. Läs genom varje deluppgift (t.ex. hela uppgift 2.1) innan du utför den.

  4. En viss mängd dokumentation av era lösningar krävs för denna labb. Kommentera!

  5. Du får använda andra labbmiljöer från och med denna labb – men koden måste lämnas in i IDEA-format och kontrolleras med IDEAs kodinspektioner!

Nu när du har nått längre i kursen måste du också börja följa de allmänna kraven på programkod, inklusive att fixa IDEAs varningar.

4.0. Inledning

Bakgrund 4.0.1: Skapa projekt och paket

Om du vill sätter du nu upp ett nytt IDEA-projekt för Tetris-labben. Det går också bra att fortsätta i det gamla projektet, så länge du separerar de gamla och nya klasserna från varandra genom att lägga dem i olika (under)paket.

Att göra 4.0.1: Skapa projekt och paket

  1. Skapa eventuellt ett nytt projekt enligt instruktionerna i labb 1. Kom ihåg att ändra More Settings / Project format till ".ipr (file based)"!

  2. Skapa ett paket för Tetris, med lämpligt namn. Se tidigare instruktioner vid behov. Följ samma namngivningsregler för paket.

4.1. Spelplan och "kvadrater"

Syfte: Modellering av spelbräde!

Det första steget är att skapa lämpliga datatyper för spelets nuvarande tillstånd – speciellt en typ för själva spelplanen, som vi kan kalla Board. Vi vill speciellt fokusera på hur man tänker när man modellerar. Därför går vi genom steg för steg hur vi har tänkt för att komma fram till de föreslagna datatyperna.

Info: Tetris-terminologi!

Tetris har en spelplan där man steg för steg lägger till tetrisblock som "ramlar ner" uppifrån. Varje tetrisblock är tekniskt sett en tetromino, ett specialfall av en polyomino (tetra=4): Det består av fyra sammanhängande kvadrater i en viss färg och ett visst mönster, där totalt 7 olika mönster är möjliga. Nedan syns ett exempel från Wikipedia. Standardnamnen på dessa block är I, J, L, O, S, T och Z.

Info: Separera information från visning

När vi skapar datatyperna för spelbräden och brickor ska vi tänka på en princip bakom designmönstret Model-View-Controller (MVC), som vi kommer att gå genom på föreläsningarna:

Det är bra att separera den grundläggande informationen om någonting, modellen, från hur den visas, vilket hanteras av vyn, och hur den kontrolleras via ett användargränssnitt, controllern. Detta hänger ihop med Single Responsibility Principle.

Mer specifikt vill vi se till att spelplanen, vår Board-klass, enbart innehåller information om vilka block och kvadrater som ligger var inom spelplanens rutnät – inte om hur detta ska visas på skärmen (färger, exakta GUI-koordinater, och så vidare). Senare kan vi välja att "visualisera" modellen i textformat, i 2D-format, med 3D-grafik, eller på många andra sätt.

Hade vi blandat ihop allt i samma klass skulle det bli en sammanblandning av för mycket funktionalitet. Dessutom vore det jobbigt att lägga till nya visningssätt, och det skulle vara mycket svårare att t.ex. göra om spelet till ett nätverksspel.

Bakgrund 4.1.1: Kvadrater

Nu ska vi börja tänka genom alternativ för modellering. Läs genom hela rutan innan du börjar genomföra detta.

Hur representerar vi spelplanens nuvarande "utseende"? Vi kunde tänka oss att spelplanen (Board) skulle lagra en lista av de hela tetrisblock som har ramlat ner samt x- och y-positioner för dessa block på planen. Men när ett tetrisblock väl har "fallit på plats" kan vissa av dess kvadrater så småningom försvinna medan andra finns kvar (att fulla rader försvinner är ju själva poängen med spelet). Detta vore det rätt krångligt att representera med en lista över tetrisblock eftersom vi skulle behöva hålla reda på vilka delar av varje tetrisblock som fortfarande finns kvar.

Istället kan vi modellera på följande sätt: För varje position (x,y) på spelplanen, är den tom, och om inte, vilken typ av tetromino tillhör kvadraten på den positionen? Varje gång ett block faller på plats läggs det då in information om fyra nya kvadrater på lämpliga koordinater i Board-objektet. Vi behöver inte hålla reda på vilket tetrisblock varje enskild kvadrat kom från, och vi behöver inte ens separata objekt för varje kvadrat -- bara separata objekt för varje kvadrattyp.

Hur representerar vi då en "typ av kvadrat"? Vi vill kunna representera exakt 8 olika värden: "här finns ingen kvadrat", "här finns en kvadrat från ett I-block", "här finns en kvadrat från ett T-block", och så vidare. Java stödjer enum-klasser, som är gjorda just för situationen när en klass har ett fast antal värden. ("Enum" kommer från "enumeration", dvs. "uppräkning". Vi vill räkna upp de 8 värden som finns, och sedan ska man inte kunna skapa nya.)

Vi ska därför skapa en enum-typ med namn SquareType. Eftersom vi troligen inte har hunnit fram till enums i föreläsningarna än, ger vi lite extra hjälp nedan.

En SquareType ska bara indikera vilken typ av tetromino som har resulterat i en viss kvadrat på skärmen. Den ska däremot inte ha några metoder för att faktiskt rita upp en kvadrat till exempel i en viss färg. Den bestämmer inte heller vilken färg kvadraten ska ha, eller om kvadraten ska visualiseras "platt" eller i pseudo-3D som i exemplet ovan (eller med en riktig 3D-motor där man kan rotera spelplanen), och så vidare. SquareType är ju enbart en del av modellen.

En vy kommer vi att skapa senare, och då är vi till och med fria att visualisera i textformat om vi vill, genom att konvertera varje enumkonstant (som det inte finns särskilt många av) som ett specialtecken som "#" eller "%". Inom schack skulle "pjäsmodellen" inte heller innehålla information om hur en kung skulle se ut på skärmen, men det kan senare visualiseras i text eller med bilder.

Att göra 4.1.1: Kvadrater

  1. Skapa en enum-klass genom att högerklicka på Tetris-paketet i IDEA, välja New | Java Class och sätta "Kind" till Enum. Ge klassen namnet SquareType.

  2. Lägg in följande inom klamrarna:
    EMPTY, I, O, T, S, Z, J, L

    Klassen ser alltså ut så här:

    public enum SquareType
    {
        EMPTY, I, O, T, S, Z, J, L
    }
    

    Detta betyder helt enkelt att typen SquareType har värdena SquareType.EMPTY, SquareType.I, SquareType.O, och så vidare. Eventuellt klagar IDEA på att namnen är för korta. Detta kan du ignorera.

    Här finns mer information om enum-typer om du skulle vara nyfiken på mera detaljer innan föreläsningen om datatyper.

Använda arrayer

Vi kommer strax att börja använda arrayer, en sorts listor med fixerad längd. Om du kommer hit innan vi har gått genom hur detta fungerar kan du se följande kortfattade exempel, eller titta närmare i Java Tutorial, en av våra kursböcker.

Första raden skapar en 2-dimensionell array. Andra raden används för att komma åt en rad i arrayen, tredje raden visar hur man kommer åt en cell, och fjärde raden visar hur man ändrar en cell.

    SquareType[][] array = new SquareType[rows][columns];
    SquareType[] rowZero = array[0];
    SquareType row5col9 = array[5][9];
    array[5][9] = SquareType.EMPTY;

Bakgrund 4.1.2: Spelplan

Nu behöver vi en spelplansklass, Board, som innehåller information om vilken typ av kvadrat som för tillfället finns på varje position på spelplanen.

Ett Board t.ex. innehålla en tvådimensionell array av lämplig storlek, där varje element är en SquareType. Att arrayer har ett fixerat antal element fungerar utmärkt i Tetris, eftersom spelplanens storlek inte ändras under spelets gång.

Board behöver också ha en konstruktor som tar en bredd och höjd på spelplanen som argument och skapar en lämplig tvådimensionell array. Detta verkar vara det enda vi behöver just nu, men fler fält och metoder kommer säkert att tillkomma när vi kommer längre med spelet.

Att göra 4.1.2: Spelplan

  1. Skapa klassen Board i Tetris-paketet.

  2. Ge klassen fältet private SquareType[][] squares. Detta deklarerar (en pekare till) en tvådimensionell array av SquareType-värden.

  3. Ge klassen en konstruktor som tar parametrarna width och height. Låt konstruktorn sätta squares till new SquareType[height][width]. Detta skapar den faktiska arrayen som fältet ska peka på.

  4. Alla positioner i arrayen är nu null, dvs. vi har en array av pekare som inte pekar på några verkliga objekt. Se till att konstruktorn fyller alla positioner i arrayen (alla kolumner i alla rader) med "tomma kvadraten" istället.

  5. Gör en main-metod som helt enkelt skapar ett Board med valfri storlek. Testkör detta för att se att programmet inte kraschar, t.ex. genom att man försöker skriva utanför arrayens gränser.

Info: Gömma data

Varför skulle den tvådimensionella arrayen vara privat i Board? För att vi vill att spelplansklassen ska ha full koll över hur planen manipuleras – andra klasser får anropa metoder i planen för att be den göra något, men de får inte gå in och ändra direkt i arrayen. Man ska inte heller skapa en metod som returnerar hela arrayen, eftersom det låter utomstående manipulera den direkt.

Bakgrund 4.1.3: Ramar

Längre fram kommer vi att låta tetrominos flytta runt på spelplanen. Då kommer vi att behöva hantera kollisioner med existerande tetrominos på planen, så att vi inte flyttar något genom något annat.

Vi kommer också att behöva kolla om användaren försöker flytta en tetromino utanför spelplanens gränser. Hur gör vi detta? Ett sätt är att använda olikheter som räknar med tetrominons storlek och brädets storlek. Ett annat sätt är att utnyttja kollisionskoden, som ju ändå måste finnas. För att göra detta skapar vi ett speciellt enum-värde OUTSIDE som vi sedan lägger som en "ram" runt spelplanen. Ett försök att flytta en tetromino utanför planen blir då helt enkelt en "krock".

Att göra 4.1.3: Ramar

  1. Lägg till värdet OUTSIDE i SquareType (i den kommaseparerade listan av värden).

  2. Se till att Board-konstruktorn lägger in en "ram" av värdet OUTSIDE i arrayens "utkanter".

  3. Testkör igen.

4.2. Utskrift av spelplan

Syfte 4.2.1: Visualisera snabbt

Vi vill gärna kunna visa spelplanen för att se om vi har gjort rätt. Istället för att gå direkt på grafiken kommer vi i ett första steg att köra textbaserat. Dels är detta enklare och gör att vi snabbt kan komma vidare, dels kan det vara användbart för debuggning av programmet, och dels är det ett steg i att demonstrera användningen av flera vyer i Model-View-Controller-mönstret och användningen av strängar i Java.

Bakgrund 4.2.1: Vy-klass, förberedelser

Vy-klassen som ska skapas behöver kunna få reda på hur stort ett Board är och vilken typ av kvadrat (om någon) som finns på en viss position. Detta ska den så klart inte få reda på genom att själv titta på fälten i Board (de ska ju vara privata). Istället krävs nya metoder i Board. De metoderna ska bland annat returnera information om en specifik position – returnerar du hela arrayen kan den ju ändras utifrån, och du låser också fast dig för hårt vid arrayrepresentationen.

Att göra 4.2.1: Vy-klass, förberedelser

  1. Skapa getters för width och height i Board samt en metod för att hämta en SquareType ur en cell i squares.

Bakgrund 4.2.2: Strängmanipulation

För att inkrementellt bygga upp en lång sträng använder man lämpligen klassen StringBuilder och dess append()-metoder. För att lägga till en radbrytning efter varje rad används "\n" (backslash n). När strängen är klar tas den fram som String-objekt med toString(). Här är ett exempel:

   // Create new StringBuilder.
   StringBuilder builder = new StringBuilder();
   
   // Loop and append values.
   for (int i = 0; i < 5; i++) {
       builder.append("abc ");
   }
   // Convert to string.
   String result = builder.toString();
   
   // Print result.
   System.out.println(result);
		       

Givet en SquareType behöver vi veta vilken symbol den ska illustreras med. Man kunde tänka sig att detta skulle läggas in direkt i själva SquareType-objektet men då har vi blandat ihop modellen (SquareType) med ett specifikt sätt att visa modellen på (text). Istället får vi låta vyn känna till vilka SquareTypes som finns och ha en hjälpmetod som returnerar symbolen motsvarande en viss SquareType.

Att göra 4.2.2: Strängmanipulation

  1. Skapa klassen TetrisTextView, med metoden convertToText(Board) som returnerar en strängrepresentation av ett modellobjekt – den skriver alltså inte ut på skärmen själv utan returnerar en sträng som anroparen kan skriva ut. Tomma rutor ska då representeras med t.ex. mellanslag eller bindestreck, och olika typer på kvadrater (SquareTypes) ska representeras som textsymboler, t.ex. "#", "%" och "-".

    Du behöver en nästlad loop som itererar över rader och kolumner i lämplig ordning. Inuti loopen kan man använda en switch-sats med ett case för varje SquareType-värde, något som är möjligt på grund av att SquareType är en enum. Det vill säga, om elementet på en viss position är SquareType.EMPTY lägger du till ett mellanslag, om elementet är SquareType.I lägger du till en annan symbol, osv.

    Glöm inte radbrytningarna "\n" mellan raderna.

    Metoden kan vara static eftersom den inte behöver tillgång till tillståndet i en specifik TetrisTextView.

  2. Skriv en testklass, BoardTest, som skapar en tom spelplan, konverterar den till en sträng med hjälp av TetrisTextView-klassen, och skriver ut resultatet. När det är klart har du åstadkommit ett testbart ramverk och kan se att du har gjort konkreta framsteg!

4.3. Slumpning av spelplan

Bakgrund 4.3.1: Test

För vår testning vill vi också ha ett sätt att slumpa fram en spelplan. Detta kan ligga i en separat hjälpklass eller läggas in i klassen Board.

Att göra 4.3.1: Test

  1. Skapa en metod som kan användas för att generera en spelplan som har fast storlek men slumpmässigt innehåll.

    Klassen java.util.Random och metoden nextInt(n) kan användas för att hitta ett pseudoslumptal mellan 0 och n-1.

    Med hjälp av klassmetoden (statiska metoden) SquareType.values() får vi fram en array som innehåller alla värden i enum-typen SquareType, vilket sedan kan indexeras med slumptalet.

  2. Ändra klassen BoardTest så att den slumpar fram och skriver ut 10 spelplaner. Testkör.

4.4. Tetrisblock / tetrominoes

Bakgrund 4.4.1: Polyominos

Efter spelplanen kommer själva Tetrisblocken. Det finns totalt 7 varianter i grundspelet, och vi kan tänkas vilja utöka detta på olika sätt i framtiden – genom nya former (utökning till pentominoes med fem kvadrater per block, penta=5) eller andra varianter (block som exploderar). Frågan är då hur man ska implementera block på bästa sätt för att tillåta sådana utökningar.

  • En generell abstrakt klass + en konkret subklass per blocktyp (L, T, ...)? Det är så klart en möjlighet, men om vi tänker lite närmare på spelet inser vi att blocken egentligen inte beter sig olika. Därför skulle vi egentligen inte ha så mycket nytta av de olika subklasserna – vi skulle till exempel inte behöva "overrida" metoder från den generella klassen för att utnyttja subtypspolymorfism, och alla klasser skulle behöva samma fält för att lagra information. Det enda som skulle skilja sig mellan klasserna är formen på varje block, och detta är data, inte beteende.

    Viktig regel: Skapa bara nya klasser om de verkligen har olika beteende som ger större skillnad i koden. Skapa inte nya klasser när det räcker med att några värden skiljer sig.

  • En enda klass? Det verkar vara ett bättre val. Och om vi sedan vill implementera exploderande block kan vi göra det med en enda ny subklass, snarare än sju olika subklasser ("exploderande L", "exploderande O", osv).

Vi skapar därför en enda blockklass, som vi kan kalla Poly (kort för Polyomino).

Vi behöver ett sätt för varje Poly-objekt att tala om hur det ser ut. Detta är inte en visualiseringsaspekt utan en del av den fundamentala modellen för ett block, så det ska definitivt finnas med i Poly-klassen. Eftersom vi ska placera ut Poly-objekt på vårt Board kan vi med fördel representera denna på samma sätt. Vi väljer därför en tvådimensionell array för vår Poly.

Tanken är att klassen Poly ska kunna användas för vilken typ av block som helst, inklusive t.ex. block med 3 eller 5 kvadrater. Därför vill vi inte direkt i Poly hårdkoda kunskapen om exakt vilka blocktyper som finns. I stället låter vi konstruktorn ta in en tvådimensionell array av kvadrater som parameter. Den exakta storleken på arrayen bestäms då av den som anropar konstruktorn.

Att göra 4.4.1: Polyominos

  1. Implementera klassen Poly enligt beskrivningen ovan.

    Klassens konstruktor ska alltså ta in en array som beskriver konfigurationen hos en godtycklig polyomino. Sådana arrayer kommer sedan att skickas in av TetrominoMaker i nästa uppgift – det är där de 7 specifika blocktyperna specificeras.

Bakgrund 4.4.2: TetrominoMaker

Vi behöver något sätt att skapa de sju sorternas Poly.

För att uppnå detta kan vi skapa en separat klass, TetrominoMaker. Klassen kan t.ex. ha dessa metoder:

    public int getNumberOfTypes();
    public Poly getPoly(int n);

En anropare kan då anropa getNumberOfTypes() för att fråga om hur många blocktyper som finns (just nu 7).

Anroparen kan sedan slumpa fram ett tal mellan 0 och getNumberOfTypes()-1 och anropa getPoly() med detta som argument. Metoden getPoly() returnerar då ett nytt Poly-objekt av den givna typen.

getPoly() gör detta genom att skapa en 2D-array av lämplig storlek, fylla den med kvadrater motsvarande det begärda blocket, skapa en ny Poly för denna array, och returnera det nya objektet.

Vilken storlek ska då arrayen ha? För att underlätta för senare rotation av blocken är det en fördel att använda en storlek på 2x2, 3x3 eller 4x4, beroende på vilken tetromino som den innehåller. Arrayerna kan då skapas enligt vänstra kolumnen i följande bild (där de övriga kolumnerna visar hur blocken ser ut när de roteras, vilket vi kommer till senare). Blocket O ("kvadraten") ser ut att ligga i en array med storlek 4x3, men för att förenkla det för oss själva kan vi helt enkelt lägga det i storleken 2x2 istället.

Bilden kommer från en beskrivning av det standardiserade rotationssystemet.

Att göra 4.4.2: TetrominoMaker

  1. Implementera klassen TetrominoMaker enligt beskrivningen ovan.

    Tänk på att inte skapa för långa metoder – om getPoly() blir för lång när du lägger in skapandet av alla blocktyper i den metoden kan du bryta ut delar till sju olika hjälpmetoder istället.

Bakgrund 4.4.3: Tetrisblock på spelplanen

Vår spelplan kan representera de block som redan har fallit ner. Detta representeras som en array av SquareTypes, vilket ger oss all information vi behöver för att rita upp det som är kvar av de block som fallit ner. Nu behöver den också kunna representera ett block som är på väg att falla ner, så att all information om spelets nuvarande tillstånd sparas på ett och samma ställe. Hur åstadkommer vi det?

Ett alternativ är att det fallande blocket också representeras i vår nuvarande datastruktur som en uppsättning kvadrater / SquareTypes i den tvådimensionella arrayen. Det gör att vi inte behöver ändra uppritningsfunktionen (TetrisTextView). Å andra sidan måste vi i varje steg, när blocket faller eller roteras, ta bort dess SquareTypes från en position i arrayen och lägga till dem i en ny position. Detta kan bli lite omständigt.

Det andra alternativet är att ha en särskild representation för det fallande blocket, med ytterligare fält i Board som representerar det fallande blocket (eller null om inget block faller) och dess position. Då behöver vi ändra uppritningsfunktionen, men vi förbereder oss samtidigt för tänkbara utökningar där block inte nödvändigtvis faller och roterar med ett "hopp" från ett läge till ett annat utan animeras jämnt och mjukt.

I den här labben ska det andra alternativet användas. Det kräver nya fält och metoder i Board samt utökningar i TetrisTextView.

Just nu (utan mjuka animeringar) gäller alltså för varje position (x,y) som ska ritas ut (läggas till i textsträngen): (1) Är positionen upptagen i Board, gäller den SquareType som anges av Board. (2) Om positionen annars täcks av en kvadrat i ett fallande block gäller denna SquareType. (3) Annars är positionen tom.

Att göra 4.4.3: Tetrisblock på spelplanen

  1. Lägg till ett fält Poly falling i Board. Du behöver också ha koll på tetrominons position, så lägg till fält för detta. Lägg slutligen till getters så att TetrisTextView kan få ut den fallande tetrominon.

  2. Ändra i TetrisTextView så att även den fallande tetrominon ritas ut (i textsträngen). Skulle den hamna ovanpå en redan upptagen ruta (en som inte är EMPTY) så skall den fallande tetrominon skrivas ut. När vi har spelmekaniken på plats kommer inte detta att kunna ske.

4.5. Ett enkelt GUI

Bakgrund 4.5.1: Textgrafik

Det är nu dags att gå över från rent textformat till ett grafiskt gränssnitt.

Om du inte är van vid GUI-programmering med Swing i Java: Vänta till du har varit på GUI-föreläsningen.

För att förenkla steget in till ett GUI-program kommer du att börja med att skriva ett grafiskt gränssnitt som använder en grafisk textkomponent, en JTextArea, för att visa den gamla textrepresentationen av en "spelplan". Det verkar kanske som ett lite underligt sätt, men det gör dels att du kan börja med GUIt redan innan du lär dig hur man skapar egna komponenter, dels att du får ett enklare "mellansteg" så att du inte behöver göra om hela användargränssnittet på en gång. Det gör det också möjligt att göra en väldigt enkel animering av t.ex. nedfallande block genom att du i JTextArea byter ut texten i varje steg, istället för att som med System.out.println() skriva ut varje ny spelplan under den förra.

Att göra 4.5.1: Textgrafik

  1. Skapa en nya klassen TetrisFrame och gör den till en subklass till Javas fönsterklass JFrame.

  2. På något sätt ska TetrisFrame få tillgång till ett Board-objekt. Man kunde tänka sig att TetrisFrame själv skulle skapa ett Board, men det är mer flexibelt om man istället skickar in ett Board som parameter till konstruktorn. Detta följer principen att varje klass ska ha en väl avdelad uppgift att utföra och ska inte göra något som ligger utanför den uppgiften! TetrisFrames uppgift är att visa ett spelbräde och låta oss interagera med det, inte nödvändigtvis att skapa det.

  3. När du skapar en subklass måste du alltid tänka på vilka parametrar som du måste – eller vill – skicka med till superklassens konstruktor. JFrame har i och för sig en konstruktor som saknar argument, så det går att låta bli att göra något explicit anrop till superklassens konstruktor från TetrisFrames konstruktor, men då får vi ingen fönstertitel. Det är bättre att anropa den JFrame-konstruktor som tar emot en fönstertitel som parameter. Konstruktorn till subklassen TetrisFrame behöver då ange den parametern genom att först av allt anropa t.ex. super("MyWindowTitle"). Se mer information om detta i föreläsningsbildernas diskussion om konstruktorer i subklasser.

  4. I konstruktorn till TetrisFrame (eller i metoder som anropas från konstruktorn) ska du sedan bygga upp ett lämpligt användargränssnitt. Den viktigaste delen just nu är den JTextArea som ska användas till att visa själva "spelplanen". Ange i konstruktorn till JTextArea hur många kolumner och rader som ska visas – detta avgör vilken preferred size textarean ska ha. Antalet kolumner och rader får du reda på genom att fråga det Board som konstruktorn har fått som parameter, inte genom att hårdkoda!

    Hur hittar man vilka konstruktorer som finns? I IDEA, skriv "new JTextArea(" och tryck Ctrl-P. Då får du se alla varianter på parametrar. Välj en av dem och tryck Ctrl-Q så får du dokumentation för parametrarna.

  5. Redan när en TetrisFrame skapas måste textarean ges sitt första innehåll. Här förutsätter vi att du har skrivit en TetrisTextView med en metod som returnerar en sträng, inte en metod som skriver ut en spelplan direkt till t.ex. System.out. Då kan du använda den metoden för att få en sträng motsvarande ett Board, och sedan använda textareans metod setText() för att sätta rätt text. Att uppdatera textarean när spelplanen ändras blir en senare uppgift.

  6. I övrigt behövs en del kod i konstruktorn för att definiera layouten (inklusive att sätta fönstrets layouthanterare), se till att fönstret visas, osv. Om du kommer hit innan GUI-föreläsningen kan vi ge följande tips, utöver läroböckerna:

    • Layouten för ett fönster hanteras av en layouthanterare, inte genom att man anger koordinater för komponenter. En lämplig start kan vara att fönstret sätter sin layouthanterare med this.setLayout(new BorderLayout()).

    • Fönstret kan sedan lägga till textarean med this.add(textarea, BorderLayout.CENTER). Detta placerar textarean "i mitten" i fönstret. Eftersom vi inte har lagt till några andra komponenter i fönstret kommer textarean att ta upp hela utrymmet i fönstret.

    • För att ge fönstret en storlek och göra det synligt: frame.pack(); frame.setVisible(true);

  7. Testklassen behöver ändras så att den skapar ett Board och öppnar ett TetrisFrame-fönster. När testet körs ska slutresultatet vara att du ser en lagom stor TetrisFrame med en framslumpad spelplan (inte 10 spelplaner, som tidigare). Just nu finns ingen händelsehantering (event handling), så du får använda "stoppknappen" i utvecklingsmiljön för att stänga av testprogrammet.

4.6. Timer för spelloop

Bakgrund 4.6.1: Timer

Så småningom behöver vi ha något sätt att driva spelet framåt, att få block att falla ner i lagom takt och så vidare.

Ett inte alltför ovanligt misstag är att man lägger in en loop som "stegar fram" ett steg, gör en paus av konstant längd, "stegar fram" nästa steg, och så vidare. Problemet är att när pausen är av konstant längd kommer tiden från starten av ett steg till starten av nästa att variera, beroende på hur lång tid själva steget tar, vilket är olika beroende på dator och CPU-belastning på datorn. Vi får göra på något annat sätt.

Som tur är finns en klass som heter javax.swing.Timer som vi kan använda för att få ett "steg" i spelet att köras regelbundet. Blanda inte ihop den med andra klasser som heter Timer, t.ex. java.util.Timer!

I den här uppgiften ska vi bara göra en enkel utforskning av Timerklassen. Senare, när vi har gått vidare till en fullständig grafisk komponent, kommer vi att titta närmare på lämplig modellering av spelet som helhet och var spelloopen egentligen ska placeras.

Det vi behöver veta kan vi då lära oss av följande exempel och beskrivningen under.

    final Action doOneStep = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
            // Gå ett steg i spelet!
        }
    };
    
    final Timer clockTimer = new Timer(500, doOneStep);
    clockTimer.setCoalesce(true);
    clockTimer.start();
    ...
    clockTimer.stop();

En Timer ser alltså till att Swings egna händelsehanteringstråd anropar en viss handling ("action") med jämna mellanrum. I detta fall är det 500 millisekunder mellan anropen, och det som anropas är den handling som vi kallade doOneStep. Genom att anropa setCoalesce(true) ser vi till att anropen inte "köas upp" om ett visst anrop till doOneStep() skulle ta för lång tid. Vi kan starta timern när spelet börjar och stoppa den när spelet är över.

I övrigt kan även clockTimer.setLogTimers() vara intressant för felsökning. Se även Javadoc-dokumentationen som vi länkar till ovan!

Att göra 4.6.1: Timers

  1. Skapa någonstans en Timer som kör en handling med regelbundna mellanrum, en gång i sekunden. Handlingen ska slumpa om spelplanen (inte byta ut Board-objektet utan ändra i det!) på motsvarande sätt som du gjort tidigare, och visa resultatet i textarean som du skapade i en tidigare uppgift. Handlingen behöver alltså på något sätt få tillgång till både spelplan och textarea. Slutresultatet ska helt enkelt bli att du ser en enkel "animering" på skärmen medan programmet körs. Du behöver inte kunna stoppa timern.

4.7. Mer GUI-programmering: Menyer!

Bakgrund 4.7.1: Menyer

Innan vi går vidare med själva spelmekaniken är det dags att vidareutveckla resten av det grafiska gränssnittet för spelet en liten aning.

Att göra 4.7.1: Menyer

  1. Lägg till menyer i spelet. Information om hur man gör detta finns i föreläsningsanteckningarna. Se minst till att det finns en meny med valet "Avsluta".

  2. Implementera en händelsehanterare – en ActionListener eller en Action, enligt vad vi har diskuterat under föreläsningarna. Se på detta sätt till att valet "avsluta" gör att en dialogruta visas med hjälp av JOptionPane, där man får bekräfta att man vill sluta. Vid bekräftelse ska spelet avslutas med hjälp av System.exit(0).

4.8. En grafisk spelplan

Bakgrund 4.8.1: Grafik del 1 – komponenten

Det är nu dags att gå över från en textbaserad visning till en helt grafisk.

Om du inte är van vid grafikprogrammering med Graphics2D i Java: Vänta till du har varit på grafik-föreläsningen.

Att göra 4.8.1: Grafik del 1 – komponenten

  1. Skapa en TetrisComponent-klass som är subklass till JComponent.

  2. Låt TetrisComponent ha en pekare till det Board som den visar. Den behöver alltså ett fält som pekar på ett Board, och den behöver en konstruktor som tar ett Board som parameter.

  3. Implementera metoden getPreferredSize() så att den returnerar den storlek du helst vill ha för den grafiska visaren. Enheten är pixlar. Om du vill anpassa komponentstorleken till skärmstorleken har vi gått genom på föreläsningarna hur man får fram skärmupplösningen. Tänk bara på att TetrisComponent-komponenten inte kan vara riktigt så stor, eftersom menyer, knappar, ramar och annat också tar plats.

Grafik

Snygg grafik är inget vi premierar i den här labben. Experimentera gärna, men spendera inte alltför mycket tid på det. Enfärgade kvadrater med 1 pixels mellanrum duger gott när syftet är att lära sig objektorientering!

Bakgrund 4.8.2: Grafik del 2 – utritningen

En JComponent behöver kunna rita upp sig själv när som helst, när Swings bakgrundstråd anropar den. Vi skall därför implementera metoden paintComponent() så att den ritar upp spelbrädet som det ser ut just nu. Om metoden blir stor kanske den behöver delas upp i delmetoder / hjälpmetoder för att förbättra läsbarheten.

Tänk på att paintComponent() behöver rita upp både bakgrunden, de kvadrater som har ramlat ner på spelplanen, och det block (om något) som håller på att ramla ner.

Fundera på hur paintComponent() ska veta vilken färg (java.awt.Color) den ska använda för varje SquareType.

  • Ett sätt är att använda en switch-sats med en gren för varje SquareType.

  • Ett annat sätt är att den har en EnumMap<SquareType,java.awt.Color> som lagrar mappningen. En EnumMap är en mappning, en uppslagningstabell liknande det som kallas dictionaries i Python – i detta fall med en SquareType som nyckel och en Color som värde. I Java finns dock ingen specialsyntax för mappningar, utan det är en klass som alla andra, med metoder som put() och get().

    Med denna lösning kan din komponent till och med ta in en EnumMap som konstruktorparameter, så kan den som skapar komponenten lätt konfigurera uppslagstabellen uppslagningstabellen (och därmed färgerna) utifrån – betydligt mer flexibelt än att hårdkoda en switchsats i paintComponent().

paintComponent() exempel

För att rita ut grafik används override på funktionen paintComponent() som ärvs från JComponent. Det kan t.ex. se ut så här:

    
    @Override
    protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		final Graphics2D g2d = (Graphics2D) g;

		g2d.setColor(Color.GREEN);
		g2d.drawRect(a, b, c, d);
	}

Här anropas super.paintComponent() för att rensa bakgrunden i komponenten. I exemplet vill vi rita ut en grön rektangel med hjälp av ett Graphics2D-objekt, men får ett Graphics-objekt som inparameter. Därför castas detta först till Graphics2D innan vi ritar ut rektangeln. Det finns många olika sätt att rita i en komponent och detta är bara ett exempel. Mer information finner du på föreläsningarna samt nätet.

Att göra 4.8.2: Grafik del 2 – utritningen

  1. Implementera paintComponent() enligt beskrivningen ovan.

  2. Ändra i det tidigare GUIt så att TetrisComponent nu används istället för TetrisTextView.

  3. Ändra i testklassen, eller skriv en ny testklass, så att den Timer som introducerades tidigare ber din TetrisComponent att rita om sig istället för att manipulera den textarea vi brukade använda. Slutmålet är att brädet ska slumpas om med regelbundna mellanrum och att varje ny konfiguration ska visas på skärmen i TetrisComponent-komponenten som finns i en JFrame. För tillfället kan du be din TetrisComponent att rita om sig genom att timerhandlingen direkt anropar repaint() i TetrisComponent-objekten. I en kommande uppgift ska vi använda ett mer principiellt sätt att få TetrisComponent att uppdatera sig vid ändringar.

Magiska konstanter

Här är det läge att påpeka att du ska undvika magiska konstanter, i betydelsen "konstanter som används i koden utan någon förklaring av var de kommer ifrån". Om t.ex. en kvadrat ritas ut i en konstant storlek av 30x30 pixel är det bra att lägga in namngivna konstanter som private final static int SQUARE_WIDTH = 30; i klassen och sedan använda denna konstant varje gång du refererar till bredden. Då blir det mycket lättare att läsa uttrycken och att förstå hur koden fungerar. (Vi använder "final" för att konstanten inte ändras, "static" för att vi inte behöver en kopia av konstanten för varje TetrisComponent som skapas.)

Speciellt svårt att läsa magiska konstanter är det om du gör beräkningar som utgår från dem och sedan stoppar in de uträknade värdena i koden. Till exempel är det oftare lättare att förstå betydelsen hos "if (x > SQUARE_WIDTH - MARGIN)" än "if (x > 27)", även för den som vet att SQUARE_WIDTH är 30 och MARGIN är 3.

4.9. Observer / Observable

Bakgrund 4.9.1: Lyssnare och notifiering

I det program du nu har skrivit har du själva fått hitta på ett sätt att få spelplanen att ritas om när något ändras. Det föreslagna sättet har varit att en timerhandling som utförs med regelbundna mellanrum både ser till att spelmodellen uppdateras (ett block faller nedåt) och att TetrisComponent därefter ritar om sig. Problemet med den lösningen är att timerhandlingen måste känna till precis vilka som vill veta när spelbrädet ändrar sig. Vi får därmed en alldeles för stark koppling mellan två klasser (timerhandling och TetrisComponent) som egentligen inte alls borde behöva känna till varandra.

Ett annat sätt är att använda designmönstret Observer. I korthet innebär detta att vi ser spelplanen som något "observerbart" (Observable). Varje gång planen förändras på något sätt (t.ex. genom att ett block faller ner ett steg) kan den själv informera en eller flera "observatörer" (Observer). Varje observatör kan då själv registrera sig som intresserad av vad som händer med ett visst Board.

Fördelen med detta tankesätt är bland annat att timerhandlingen kan fokusera helt på att driva spelet framåt ett steg. När spelet drivs framåt ett steg är det sedan spelbrädet som talar om för alla intresserade att något har hänt. En av dessa intresserade råkar vara TetrisComponent, som tidigare har registrerat sitt intresse.

Vi har faktiskt redan sett en användning av detta mönster i Javas GUI-bibliotek. Alla komponenter är observerbara, och alla lyssnare är observatörer. Metoder som addActionListener() lägger till en lyssnare, en observatör, som är intresserad av en specifik typ av handling. Klasser som JButton känner inte till era specifika lyssnare i förväg, men de har ett sätt för lyssnarna att registrera sitt intresse.

Att göra 4.9.1: Lyssnare och notifiering

  1. Skapa ett interface BoardListener med en metod public void boardChanged();

  2. Lägg till i klassen Board ett privat fält som innehåller en lista av BoardListeners. Lägg också till en metod public void addBoardListener(BoardListener bl) som adderar den givna lyssnaren till listan. I detta spel kommer vi inte att behöva något sätt att ta bort lyssnare, även om man så klart kan lägga till även en sådan metod för att vara mer fullständig.

  3. Skapa i Board en privat metod private void notifyListeners() som loopar över alla element i lyssnarlistan och anropar deras boardChanged()-metoder.

  4. Se till att alla publika metoder i Board som ändrar på spelplanen anropar notifyListeners() på slutet. Detta inkluderar metoder som t.ex. ändrar koordinater på det block som håller på att ramla ner. Du måste också se till att göra på samma sätt i nya metoder du lägger till senare i labben.

  5. Ändra TetrisComponent så att den implementerar gränssnittet BoardListener. Implementera metoden boardChanged() så att den anropar repaint().

  6. Se till att timerhandlingen som driver animeringen framåt inte längre anropar repaint() själv, som i tidigare lösning. Ändra istället på era testklasser så att TetrisComponent-objektet du skapar adderas som en lyssnare på spelbrädet.

Resultat

Slutresultatet av denna ändring ska bli att allt ser ut precis som tidigare (men att koden har en bättre struktur).

4.10. Fallande block

Bakgrund 4.10.1: Fallande block

Nu är det dags att börja implementera den riktiga spelmekaniken i Tetris. Vi gör detta i flera steg. I stället för att som tidigare att utnyttja timerhandlingen till att slumpa fram en helt ny spelplan full av kvadrater ska den nu driva spelet ett steg framåt. Det är dock en dålig idé att lägga spelmekanik och spelregler i själva timerhandlingen – då skulle vi återigen blanda ihop två helt olika typer av funktionalitet.

En bättre variant är att du skapar en tick()-metod i Board och anropar den metoden från timerhandlingen. Metoden tick() kan sedan veta hur spelet drivs fram ett steg. Då hamnar kunskapen om spelets regler i Board, medan timerhandlingen används uteslutande för att driva spelet framåt i rätt fart. (Ett alternativ kan vara att separera spelmekaniken ytterligare och placera den i en TetrisGame-klass.)

Metoden tick() behöver titta om spelplanen just nu innehåller ett block (Poly) som håller på att ramla ner. I så fall flyttar den det blocket ett steg nedåt. Annars slumpar den fram en blocktyp (T, L, ...) och placerar ett nytt block av den typen överst på spelplanen, centrerat. Mer spelmekanik tillkommer senare.

Eftersom du nu har programmeraten del i Java börjar vi också gradvis beskriva uppgifterna lite mindre detaljerat. Fråga gärna assistenten om du vill ha hjälp!

Att göra 4.10.1: Fallande block

  1. Ändra spelet så att det utgår från en tom spelplan istället för en framslumpad spelplan.

  2. Implementera tick() enligt beskrivningen ovan.

Resultat

Slutresultatet i denna uppgift blir ett testprogram där ett block slumpas fram och sedan faller nedåt tills det "faller av skärmen" (och då fortsätter det falla i all oändlighet, även om detta inte syns), eller orsakar ett undantag (Exception) beroende på implementationen. Att stoppa blocket kommer i nästa uppgift.

4.11. Ändlig skärmstorlek

Bakgrund 4.11.1: Stoppa fallande block

Nu är det dags att få blocken att stanna när det nått botten av skärmen / spelplanen. Detta kräver så klart att du tittar på det fallande blockets form och beräknar när den nedersta kvadraten i blocket når botten.

Om ett block (Poly) redan har fallit ner till botten av skärmen och försöker falla ett steg till – vilket inte går – ska dess kvadrater överföras till själva spelplanen i blockets slutliga läge ("läggas in i den tvådimensionella arrayen"). Spelplanen ska sedan övergå i ett läge där det inte har något fallande block (t.ex. genom att pekaren till det fallande blocket återigen blir null), så att nästa tick() kan se att det är dags att slumpa fram ett nytt fallande block. Vi har alltså:

  • tick(). Ett antal gånger: Vi beräknar om det skulle gå att flytta blocket ett steg längre ner utan att någon av dess kvadrater hamnar under nedersta kanten på skärmen. Det går, så blocket flyttas till nästa position.

  • tick(). Ett antal gånger: Vi beräknar om det skulle gå att flytta blocket ett steg längre ner utan att någon av dess kvadrater hamnar under nedersta kanten på skärmen. Det går inte, så blocket är redan på positionen längst ner. Dess kvadrater kopieras in till rätt koordinater på spelplanen och själva blocket (Poly) "tas bort". Nu finns alltså inget fallande flyttbart block, men motsvarande kvadrater finns inlagda "permanent" i spelplanen. Utåt ser det ut som att spelet står still i ett tick – ingen skillnad syns.

  • tick(). Metoden ser att det inte finns något block som håller på att ramla ner, så ett nytt slumpas fram på översta positionen.

  • tick(). Som i första punkten...

Att göra 4.11.1: Stoppa fallande block

  1. Implementera beteendet som diskuterats ovan.

    Tänk på att inte lägga alltför mycket kod i en och samma metod. Bryt ut kod till hjälpmetoder vid behov. IDEA kan hjälpa till med dess Extract Method-refactoring.

Resultat

Resultatet i denna uppgift blir att nya block ständigt faller ända ner till botten. Eftersom vi ännu inte har kollisionshantering mellan blocken utan bara stoppar block vid botten av skärmen kommer blocken att helt eller delvis ersätta varandra längst ner. Kollisionshantering mellan block kommer i nästa uppgift.

4.12. Kollisionshantering och "game over"

Bakgrund 4.12.1: Kollisioner

Nu lägger vi till kollisionshantering mellan block. Med detta menas att ett block inte ska kunna "ramla förbi" ett annat utan att block ska staplas ovanpå varandra. Spelets beteende kan t.ex. vara så här:

  • tick(). Blocket flyttas ned ett steg och har nu kvadrater omedelbart under sig.

  • tick(). Blocket kan inte flyttas ett steg ned. Hanteras som tidigare genom att lägga in kvadrater i arrayen.

  • tick(). Det finns inget block som håller på att ramla ner, så ett nytt slumpas fram. Kollisionshantering upptäcker att detta block inte ens kan placeras på översta positionen, så blocket placeras inte ut och gameOver-flaggan sätts.

  • tick(). Eftersom gameOver-flaggan är satt gör vi inget.

Att göra 4.12.1: Kollisioner

  1. Utöka koden som "beräknar om det skulle gå att flytta blocket ett steg längre ner " enligt förra uppgiften. Nu räcker det inte att titta om blocket vid en hypotetisk flytt till nästa position skulle gå under kanten på skärmen. Du måste också kolla om blocket vid en sådan hypotetisk flytt skulle krocka med ett annat block som redan finns i spelplanen.

  2. När ett block har fallit ner så långt det kan och ett nytt block slumpas fram måste spelet titta efter om det nya blocket överhuvudtaget får plats på sin initialposition, centrerat längst upp. Om inte, ska spelet vara över. Detta kan t.ex. implementeras genom att en flagga sätts som gör att tick() omedelbart returnerar.

4.13. Tangentbordsstyrning

Bakgrund 4.13.1: Styra tetrominos

Dags att lägga till tangentbordsstyrning av block så att de kan flyttas åt höger och vänster. Tangentbordsstyrning kan göras med hjälp av key bindings kopplade till din TetrisComponent. Här får du också en övning i att läsa ut information ur Java Tutorial!

Tänk på att man normalt kan flytta ett block flera steg åt sidan under samma tick. Eftersom key bindings hanteras asynkront ska detta fungera automatiskt.

Att göra 4.13.1: Styra tetrominos

  1. Implementera metoder i Board som anropas vid sidledsförflyttning. Dessa metoder ska flytta det nedfallande blocket ett steg åt sidan om detta är möjligt (inga kvadrater är i vägen). Precis som alla andra metoder som ändrar på speltillståndet behöver denna metod anropa notifyListeners() för att informera alla intresserade om att något har ändrats. Däribland finns den grafiska komponenten som därmed ritas om varje gång ett block har flyttats i sidled.

  2. Lägg även till en tills vidare tom metod i Board som skall hantera rotation.

Info: Synkronisering

Den som är bekant med trådad programmering undrar kanske om det nu kan hända att Board anropas från flera trådar. Nej, faktiskt inte. Tangentbordsbindningar behandlas i Swings händelsehanteringstråd, så det är den tråden som flyttar block i sidled. Den timer vi valde att använda tidigare är javax.swing.Timer, och den anropar också sin handling (som i sin tur anropar Board.tick()) från Swings händelsehanteringstråd. Därför borde vi undvika trådningsproblem och inte behöva använda synchronized().

4.14. Borttagning av rader

Att göra 4.14.1: Försvinnande rader

  1. En av grundtankarna bakom Tetris är att en rad kvadrater "försvinner" om den blir helt full. Implementera detta, så att du kanske kan spela lite längre innan spelet tar slut!

    Blir det svårt att fylla en rad? Testa att tillfälligt tvinga spelet att bara skapa kvadrater ("O"), så kan du testa borttagningen enklare.

4.15. Rotation av tetrisblock

Bakgrund 4.15.1: Rotation

Om man ska ha en chans att fylla rader så de försvinner behöver man också kunna rotera blocken. Detta ger oss inte så mycket vad gäller objektorientering, så att implementera rotation är frivilligt.

Hur roterar man ett block? Det enklaste sättet att rotera vår tetromino är genom att flytta alla våra SquareType enligt följande bild:

Vi gör då en funktion rotate() i Poly som tar en boolean som inparameter. Här används inparametern för att bestämma om vi roterar åt vänster eller höger. Rent praktiskt behöver vi en ny tom array eller en ny Poly som vi efter hand kan stoppa in de olika rutornas innehåll i. När vi är klara byter vi till den nya arrayen/polyn.

För ytterligare ledning kan du se följande funktion, som enbart roterar åt höger. Tänk på att two wrongs don't make a right – but three lefts do, och tvärtom. Med andra ord, man kan rotera åt vänster genom att rotera åt höger tre gånger.

 public Poly rotateRight(){

    Poly newPoly = new Poly(width,height);

    for (int r = 0; r < height; r++) {
        for (int c = 0; c < width; c++){
            newPoly.squares[c][height-1-r] = 
                this.squares[r][c];
        }
    }

    return newPoly;
}

Att göra 4.15.1: Rotation

Vi upprepar: Detta är frivilligt.

  1. Implementera rotate()-funktionen och testa den.

  2. Lägg till kod för tangentbordsstyrning så att en rotationsfunktion i Board anropas då man trycker "pil upp"-tangenten. Denna funktion behöver se till att block som roteras inte hamnar ovanpå upptagna rutor, i så fall måste rotationen förhindras. Se också till att det inte går att rotera block ut ur spelplanen.

4.16. Slut – dags att demonstrera!

Avslutning

Som synes resulterar den här labben inte nödvändigtvis i ett fullständigt spel. Vi har ingen poängräkning, vi kan inte börja om efter att spelet tar slut annat än genom att stoppa och starta om hela programmet, blocken faller inte snabbare när tiden går så att spelet blir svårare, och så vidare. Det finns två anledningar till det: Dels skulle det mest kräva mer av samma typ av programmering som du redan har provat på, dels måste du ha gott om tid över till projektet.

Demonstration

  1. Det är nu dags att demonstrera slutresultatet för labbhandledaren. Följ sedan instruktionerna nedan för att lämna in koden.

Kodinlämning

Syfte

Vi har strikta instruktioner för inlämning. Vi har över 130 studenter, så om vi behöver lägga ner 10 minuter extra per student "i onödan" tar det över 20 timmar som vi kunde ha lagt på handledning och kursutveckling! Därför måste vi returnera labbar som inte följer instruktionerna för komplettering.

Inlämningsprocedur

  1. Se till att du har demonstrerat. Handledaren kan ha kommentarer redan vid demo, så du ska demonstrera före inlämning (och före deadline).

  2. Labben måste lämnas in i IDEA-format så att vi snabbt kan öppna den och navigera genom koden.

    Har du inte använt IDEA: Skapa ett projekt enligt instruktionerna i labb 1. Kom ihåg: Ändra More Settings / Project format till ".ipr (file based)"!

  3. Läs genom de allmänna kraven igen och se till att koden uppfyller dem.

  4. Kör IDEAs kodinspektioner. Det här är till stor del ett sätt att lära sig skriva kod som inte bara gör vad den ska utan även är välskriven och strukturerad.

    Förstår du inte en varning? Utmärkt! Då har du en chans att lära dig något nytt, redan innan du lämnar in koden! Läs IDEAs inbyggda beskrivningar, läs våra utökade beskrivningar, och om det inte hjälper, fråga gärna assistenten eller examinatorn.

    1. Välj Analyze | Inspect Code i menyn.
    2. Välj inspektionsprofil "TDDD78-2014-v1" och tryck OK.

    Inget automatiskt inspektionsverktyg är 100% korrekt. Det kan alltså finnas varningar som är "felaktiga". Alla sådana måste kommenteras, med god motivering. Övriga varningar ska korrigeras. Se även de allmänna kraven.

  5. Är detta en komplettering? Beskriv i så fall i filen "kompletteringar.txt" hur varje enskild kommentar från handledaren har hanterats: Vad som ändrats och var i koden, hur du har löst problemet, och annan information som är relevant för att handledaren lätt ska se vad som gjorts.

    Gör du flera kompletteringar ska gamla kommentarer vara kvar och tydligt skilda från nya kommentarer (markera med kompletteringsdatum). Detta underlättar för oss och är en del av examinationen där du visar att du förstår varför kompletteringen behövdes.

  6. Packa all källkod och andra filer som krävs tillsammans med ev. kompletteringsbeskrivning i en ZIP- eller TAR.GZ-fil.

    IDEAs projektdefinitionsfiler (.ipr, .iml) och liknande måste finnas med så att projektet enkelt kan öppnas i IntelliJ IDEA. Är du osäker, testa genom att packa upp arkivet på annan plats och öppna det i IDEA från denna plats!

    Döp filen enligt följande mönster, utan mellanslag i filnamnet. Vid komplettering använder du versionsnummer "v2", "v3" och så vidare.

    • liuid123-labb4-v1.zip
  7. Skicka in filen via epost – till handledaren under kursperioden, till examinatorn efter kursens slut. Brevets ämne ska inledas med kurskoden TDDD78 så det kan sorteras rätt. Annars kan det dröja innan brevet blir läst.

2012–2014 Jonas Kvarnström, Mikael Nilsson


Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2015-11-09