LUNDS TEKNISKA HÖGSKOLA Institutionen för datavetenskap Tentamen EDAA45 Programmering, grundkurs 2018-08-24, 08:00-13:00 Hjälpmedel: Snabbreferens för Scala & Java. Instruktioner Skriv din anonymkod + personlig identifierare här: Om du skriver icke-anonymt ange personnummer + namn i stället. Tillåtet hjälpmedel: Snabbreferens för Scala & Java. Uppgift 1 i del A består av deluppgifter som du ska besvara genom att fylla i en tabell i detta häfte. Del B innehåller uppgift 2, 3,... med svar i form av programkod som du ska skriva på separata papper. Skriv bara på ena sidan av varje inlämnat blad. Skriv din anonymkod + personlig identifierare (eller personnummer + namn om du skriver icke-anonymt) överst på varje inlämnat blad. Det ska tydligt framgå vilken (del)uppgift du löser. Detta häftet ska lämnas in tillsammans med ifyllt omslag och svaren på uppgifterna i del B. Preliminär poängfördelning Maximalt ges 100p, varav uppgift 1 omfattar 20p och resterande uppgifter omfattar 80p. För godkänt krävs 50p. Om du på uppgift 1 erhåller färre än 10p, kan din tentamen komma att underkännas utan att resterande uppgifter bedöms. Poäng och delpoäng som anges ovan och i uppgifterna är preliminära och kan komma att justeras när den slutliga bedömningen fastställs. Upplysningar För att vara tentamensberättigad ska du vara godkänd på alla obligatoriska laborationer och projekt, samt ha genomfört diagnostisk kontrollskrivning. Om du tenterar utan att vara tentamensberättigad annulleras din skrivning. För att undvika att någon skrivning annulleras av misstag kommer alla som, enligt institutionens noteringar, tenterat utan att vara tentamensberättigade att kontaktas via epost. Felaktigheter i institutionens noteringar kan därefter påtalas fram till nästa tentamenstillfälle då resterande skrivningar annulleras. Lösningar läggs ut på kursens hemsida senast dagen efter tentamen. Resultatet läggs in i Ladok när rättningen är klar. 1
2(11) Del A: Uttryck och värden. Uppgift 1, totalt 20 poäng. Följande kod finns kompilerad utan kompileringsfel och tillgänglig på classpath: 1 case class Pos(x: Int, y: Int){ 2 def +(dir: Dir): Pos = Pos(x + dir.dx, y + dir.dy) 3 } 4 5 abstract sealed class Dir(val dx: Int, val dy: Int) 6 case object North extends Dir(0,-1) 7 case object South extends Dir(0, 1) 8 case object West extends Dir(-1, 0) 9 case object East extends Dir(1, 0) 10 11 case class Apple(dim: (Int, Int), pos: Pos){ 12 import scala.util.random.nextint 13 def move(nothere: Seq[Pos]): Apple = copy(pos = { 14 var p = Pos(nextInt(dim._1), nextint(dim._2)) 15 while (nothere contains p) p = Pos(nextInt(dim._1), nextint(dim._2)) 16 p 17 }) 18 } 19 object Apple{ 20 def apply(width: Int, height: Int, nothere: Seq[Pos]): Apple = 21 (new Apple((width, height), Pos(0,0))).move(notHere) 22 } Du ska fylla i tabellen på nästa sida enligt följande. Antag att du skriver in nedan kod i Scala REPL rad för rad. För varje variabel med namn x1... x10, ange statisk typ (alltså den typ kompilatorn härleder), samt det värde variabeln får efter initialisering, eller sätt i stället kryss i rätt kolumn om det blir ett kompileringsfel respektive exekveringsfel. Vid frånvaro av fel, svara på samma sätt som Scala REPL skriver ut typ respektive värde, enligt exempel x0 i tabellen. 1 val ps = Vector(Pos(42,42), Pos(0,0), Pos(1,1) + West) 2 val a = Apple((100, 200), Pos(42, 43)) 3 4 val x0 = 41.0 + 1 5 val x1 = ps(1) 6 val x2 = new Dir(42,43) 7 val x3 = Pos(1, 0) + South 8 val x4 = ps(3) 9 val x5 = Pos(1, 0) + Pos(0, 1) 10 val x6 = a.dim._1 + a.pos.x 11 val x7 = { var x = 0; ps.foreach{p => p + 1}; x } 12 val x8 = ps.map(_.x).sum 13 val x9 = { case object NoDir extends Dir(0,0); NoDir : Dir } 14 val x10 = Apple(100, 200, ps).pos == Pos(42, 42)
3(11) Vid kompileringsfel sätt kryss. Vid exekveringsfel sätt kryss. Ange statisk typ som kompilatorn härleder om ej kompilerings- eller exekveringsfel. Ange det värde som tilldelas vid exekvering, med samma format som vid utskrift av värdets tostring, om ej kompilerings- eller exekveringsfel. x0 Double 42.0 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10
4(11) Del B. Implementation. Uppgift 2, 3 och 4, totalt 80 poäng. Spelet Snake: bakgrund, exempel och övergripande krav Du ska implementera det legendariska spelet Snake, som blev vida populärt i Sverige redan på 1980-talet genom datorn ABC80 under namnet Masken. Du ska skapa en förenklad variant av Snake för en spelare, där användaren styr en orm med målet att äta upp ett äpple. Ormen rör sig kontinuerligt steg för steg och användaren kan styra dess riktning med tangenterna W, S, A, D motsv. upp, ner, vänster och höger. Då användaren lyckas styra ormens huvud till samma position som äpplet ges poäng och äpplet återuppstår på en ny slumpmässigt vald position. Figur 1 visar en orm och ett äpple som ritats med blockgrafik. Fig. 1: En hungrig orm med grönt huvud och blå svans, samt ett läckert, rött äpple. Du ska använda Pos och Dir med subtyperna North, South, East, och West, från del A för att representera blockkoordinater och rörelseriktning. Du ska även använda Apple från del A för att representera äpplet. Figur 2 visar en ögonblicksbild ur en spelomgång med en orm som är påväg i riktning North. Ormen har en svans som är 4 block lång. Figur 3 visar en senare ögonblicksbild ur samma spelomgång efter att ormen förflyttats 4 steg och nu är på väg i riktning East. (Siffrorna anger blockgrafikfönstrets koordinater horisontellt och vertikalt i detta exempel. Dessa siffror ritas ej i det verkliga spelfönstret.) Fig. 2: En orm med huvudet i position Pos(2,4) och ett äpple i position Pos(11,3). Fig. 3: Ormen har nu närmat sig äpplet, efter ett steg i riktning North och tre steg i riktning East. Följande övergripande krav gäller: Varje gång ormen lyckas äta äpplet så ska ormen växa, användarens poäng ökas med 100, och äpplet flyttas till en ny slumpmässig position inom spelplanen som inte ockuperas av ormen. Vid varje steg som ormen förflyttas ska användarens poäng ökas med antalet svansblock. Spelet ska avbrytas om användaren styr så att ormen äter sig själv, d.v.s. huvudet styrs in i den egna kroppen, eller om användaren styr ormen utanför spelplanens gränser. Spelet ska avbrytas efter en viss maxtid. När spelet avbryts ska den sammanlagda poängsumman ska skrivas ut.
5(11) Systemets komponenter Följande delvis färdiga komponenter ingår i systemet, där du ska göra klart de delar som saknas enligt uppgiftsbeskrivningarna på efterföljande sidor: Case-klassen Snake representerar en orm; se vidare uppg. 2. Java-klassen BlockWindow erbjuder blockgrafik med hjälp av ett SimpleWindow; se vidare uppg. 3. Klassen SnakeGame (uppg. 4) implementerar spelets logik med hjälp av bl.a. Snake och BlockWindow. Spelet körs igång med hjälp av detta huvudprogram: 1 object SnakeMain { 2 def main(args: Array[String]): Unit = { 3 println("welcome to Snake for one player!") 4 println("press the W S A D keys to turn north, south, west or east.") 5 val game = new SnakeGame() // SnakeGame implementeras i uppgift 4 6 val points = game.play() 7 println(s"game Over! You got $points points!") 8 } 9 } Följande färger finns definierade i singelobjektet Color: 1 object Color { 2 val head = java.awt.color.green // ormens huvud 3 val tail = java.awt.color.blue // ormens svans 4 val apple = java.awt.color.red // äpplet 5 val erase = java.awt.color.black // bakgrunden 6 }
6(11) Uppgift 2: Snake (15p) Du ska göra klart case-klassen Snake nedan. 1 case class Snake(body: Vector[Pos]){ 2 require(body.nonempty, "snake body must not be empty") 3 4 def head: Pos =??? 5 6 def tail: Vector[Pos] =??? 7 8 def grow(dir: Dir): Snake =??? 9 10 def shrink: Snake =??? 11 12 def move(dir: Dir): Snake =??? 13 14 def isheadinsidewindow(width: Int, height: Int): Boolean = 15 head.x >= 0 && head.y >= 0 && head.x < width && head.y < height 16 17 def iseatingitself: Boolean =??? 18 19 def iseating(apple: Apple): Boolean =??? 20 } 21 object Snake { 22 def apply(width: Int, height: Int, dir: Dir): Snake =??? 23 } Du ska färdigställa de saknade implementationerna enligt följande krav och exempel: Första positionen i body motsvarar koordinaterna för blocket som utgör ormens huvud, resten är positionerna för blocken i ormens svans. Metoden head ska ge positionen för ormens huvud och metoden tail ska ge en sekvens med de positioner som utgör svansen. Ormen kan växa genom anrop av metoden grow(d), som ska ge en ny instans med en extra position i början av body med värdet head + d. Ormen kan krympa via metoden shrink, som ska ge en ny instans där sista positionen i kroppen är borttagen. Ormen kan förflyttas ett steg i en viss riktning genom anrop av metoden move(d) som då ska ge en ny instans där positionerna är förflyttade ett steg i riktning d; se exempelkörning i REPL på nästa sida. Tips: Förflyttning i ett steg implementeras lättast med hjälp av grow och shrink. Metoden iseatingitself ska ange om huvudets position ingår i svansen eller ej. Anrop av metoden iseating(a) ska ange om huvudet finns på samma position som a. Fabriksmetoden apply ska ge en orm med två svanspositioner som skapats genom att en orm bestående av enbart huvud i Pos(width / 2, height / 2) därefter växer med hjälp av grow två gånger i riktning dir. Se exempelkörning i REPL på nästa sida.
7(11) Nedan visas en exempelkörning i REPL där relevanta klasser gjorts tillgängliga på classpath: 1 > scala 2 Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181). 3 Type in expressions for evaluation. Or try :help. 4 5 scala> Snake(Vector(Pos(4,7))).grow(North) 6 res0: Snake = Snake(Vector(Pos(4,6), Pos(4,7))) 7 8 scala> Snake(Vector(Pos(4,7))).grow(North).grow(North) 9 res1: Snake = Snake(Vector(Pos(4,5), Pos(4,6), Pos(4,7))) 10 11 scala> Snake(Vector(Pos(4,7))).grow(North).grow(North).shrink 12 res2: Snake = Snake(Vector(Pos(4,5), Pos(4,6))) 13 14 scala> Snake(Vector(Pos(4,7))).grow(North).grow(North).grow(East).shrink 15 res3: Snake = Snake(Vector(Pos(5,5), Pos(4,5), Pos(4,6))) 16 17 scala> Snake(Vector(Pos(4,7))).grow(North).grow(North).move(East) 18 res4: Snake = Snake(Vector(Pos(5,5), Pos(4,5), Pos(4,6))) 19 20 scala> Snake(10,15,North) 21 res5: Snake = Snake(Vector(Pos(5,5), Pos(5,6), Pos(5,7))) 22 23 scala> Snake(10,15,North).head 24 res6: Pos = Pos(5,5) 25 26 scala> Snake(10,15,North).tail 27 res7: Vector[Pos] = Vector(Pos(5,6), Pos(5,7)) 28 29 scala> Snake(10,15,North).grow(South) 30 res8: Snake = Snake(Vector(Pos(5,6), Pos(5,5), Pos(5,6), Pos(5,7))) 31 32 scala> Snake(10,15,North).grow(South).isEatingItself 33 res9: Boolean = true 34 35 scala> Snake(10,15,North).grow(North) 36 res10: Snake = Snake(Vector(Pos(5,4), Pos(5,5), Pos(5,6), Pos(5,7))) 37 38 scala> Snake(10,15,North).grow(North).isEatingItself 39 res11: Boolean = false 40 41 scala> Snake(10,15,North).isEating(Apple((10,15),Pos(5,6))) 42 res12: Boolean = false 43 44 scala> Snake(10,15,North).isEating(Apple((10,15),Pos(5,5))) 45 res13: Boolean = true 46 47 scala> Snake(10,15,North).isEating(Apple((10,15),Pos(4,5))) 48 res14: Boolean = false 49 50 scala> :quit
8(11) Uppgift 3: BlockWindow (Java, 15p) Du ska göra klart Java-klassen BlockWindow nedan, som används för grafik som ritas med hjälp av kvadratiska block. Du ska använda denna klass i uppgift 4. 1 import cslib.window.simplewindow; 2 import java.awt.color; 3 4 public class BlockWindow { 5 private int width; 6 private int height; 7 private int blocksize; 8 protected SimpleWindow window; 9 10 public BlockWindow(int width, int height, int blocksize, String title){ 11 this.width = width; 12 this.height = height; 13 this.blocksize = blocksize; 14 window = new SimpleWindow(width * blocksize, height * blocksize, title); 15 } 16 17 public int getwidth() { 18 return width; 19 } 20 21 public int getheight(){ 22 return height; 23 } 24 25 public void clear(){ /*??? */ } 26 27 public void drawblock(int x, int y, Color color){ /*??? */ } 28 29 public String lastkeypressedorempty(){ 30 window.waitforevent(1); 31 while (window.geteventtype()!= SimpleWindow.TIMEOUT_EVENT && 32 window.geteventtype()!= SimpleWindow.KEY_EVENT) { 33 window.waitforevent(1); 34 } 35 if (window.geteventtype() == SimpleWindow.KEY_EVENT) { 36 return String.valueOf(window.getKey()); 37 } else { 38 return ""; 39 } 40 } 41 }
9(11) Du ska implementera metoderna clear och drawblock, enligt nedan krav och tips: Ett BlockWindow består av width height kvadratiska block med vardera blocksize 2 pixlar. Ett BlockWindow använder internt ett SimpleWindow enligt specifikation nedan. Positionerna i ett BlockWindow och ett SimpleWindow anges i olika koordinatsystem. Medan SimpleWindow definierar bredd och höjd i antal pixlar, räknas höjden och bredden i ett BlockWindow i antal block. Metoden clear ska göra alla pixlar i fönstret svarta med hjälp av metoden drawblock och färgen java.awt.color.black. Metoden drawblock ska rita ett block på platsen (x, y) i blockfönstrets koordinater, vilket motsvarar att platsen för blockets övre vänstra hörn är (x blocksize, y blocksize) i SimpleWindowkoordinater. Tips: Du kan rita en kvadrat genom att rita många intill-liggande linjer med hjälp av SimpleWindow-metoderna moveto och lineto. Till din hjälp har du klassen SimpleWindow enligt nedan specifikation: package cslib.window; SimpleWindow /** Creates a window and makes it visible. */ public SimpleWindow(int width, int height, String title); /** Moves the pen to a new position. */ public void moveto(int x, int y); /** Moves the pen to a new position while drawing a line. */ public void lineto(int x, int y); /** Sets the line color to col. */ public void setlinecolor(java.awt.color col); /** Waits for key press event or timeout in milliseconds. */ public void waitforevent(int timeoutmillis); /** Returns the type of the last event. */ public int geteventtype(); /** Returns the key that was pressed on a key event. */ public char getkey(); /** Key pressed event type. */ public final static int KEY_EVENT; /** Event type when waitforevent timeout. */ public final static int TIMEOUT_EVENT; Specifikationen ovan är endast ett utdrag ur dokumentationen för kursens SimpleWindow som du använt på laborationerna; övriga delar som erbjuds av SimpleWindow men som inte är relvanat här är inte med i specifikationen.
10(11) Uppgift 4: SnakeGame (50p) Klassen SnakeGame implementerar spellogiken. Du ska göra klart de delar som saknas nedan. 1 class SnakeGame( 2 val dim: (Int, Int) = (40,30), //bredd och höjd i antal block 3 val blocksize: Int = 15, //blockstorlek 4 val keymap: Map[String, Dir] = //tangenter som styr ormens riktning 5 Map("W" -> North, "S" -> South, "A" -> West, "D" -> East), 6 var gameloopdelaymillis: Int = 150, // fördröjn. i millisek. per looprunda 7 var moveappleprobability: Double = 0.01, // 1% äppelflyttrisk per looprunda 8 val gamedurationseconds: Int = 100 // maximal speltid i sekunder 9 ) { 10 val window = new BlockWindow(dim._1, dim._2, blocksize, "Snake") 11 var dir: Dir = North 12 var snake = Snake(dim._1, dim._2, dir) 13 var apple = Apple(dim._1, dim._2) 14 val t0: Long = System.currentTimeMillis 15 var points: Int = 0 16 17 def isgameover(): Boolean =??? //true om spelet ska avbrytas 18 19 def updatedir(key: String): Unit =??? //uppdaterar dir vid knapptryck 20 21 def updatesnake(): Unit =??? //uppdaterar ormen 22 23 def updateapple(): Unit =??? //uppdaterar äpplet 24 25 def eatapple(): Unit =??? //anropas om ormen äter äpplet 26 27 def play(): Int =??? //innehåller själva spelloopen 28 } Dina implementationer av de saknade delarna ska uppfylla följande krav: isgameover: Denna metod anropas i play för att avgöra om spelet ska fortsätta eller ej. Implementationen av detta predikat ska uppfylla kraven på sidan 4 om när spelet ska avbrytas. Tips: Uttrycket (System.currentTimeMillis - t0) ger antalet millisekunder som spelet pågått. updatedir: Denna metod anropas i play för att ändra rikningen då användaren tryckt på relevant tangent, via den färdiga metoden lastkeypressedorempty i BlockWindow. Du ska i implementationen av updatedir använda nyckelvärdetabellen keymap för att ändra riktning enligt tillhörande värde om key ingår som nyckel. Om key inte finns bland nycklarna ska riktningen inte ändras. updatesnake: Denna metod anropas i play varje gång ormen ska ta ett steg. Metoden updatesnake ska i tur och ordning göra följande: 1. Sudda blocken på ormkroppens positioner genom att rita med färgen Color.erase. 2. Flytta ormen ett steg. 3. Rita ormens huvud med färgen Color.head. 4. Rita ormens svans med färgen Color.tail.
11(11) updateapple: Denna metod ska anropas i play för att eventuellt flytta äpplet. Metoden updateapple ska i tur och ordning göra följande: 1. Rita äpplet med färgen Color.apple. 2. Med sannolikheten moveappleprobability ska äpplet suddas och flyttas till en slumpmässig position inom spelplanen, dock ej till någon position som ormen finns på. Tips: Du har nytta av scala.util.random.nextdouble som ger ett slumptal mellan (inklusive) noll och (exklusive) ett. eatapple: Denna metod ska anropas i play om ormen äter äpplet. Metoden eatapple ska i tur och ordning göra följande: 1. Sudda äpplet. 2. Flytta äpplet till en slumpmässig position inom spelplanen, dock ej till någon position som ormen finns på. 3. Göra så att ormen växer i sin nuvarande riktning. 4. Göra så att spelloopens fördröjning blir 80% av sitt nuvarande värde avrundat till närmaste heltal. På så sätt ökar ormens hastighet för varje äpple som äts. 5. Öka poängen med 100. play: Denna metod anropas av huvudprogrammet för att köra igång spelet. Metoden ska börja med att göra fönstret svart och sedan köra en loop som pågår så länge spelet inte ska avbrytas. Vid spelets avbrott ska användarens poäng returneras. I loopen ska följande göras i tur och ordning: 1. Uppdatera riktningen. 2. Uppdatera ormen. 3. Kontrollera om ormen äter äpplet och i så fall vidta åtgärder. 4. Uppdatera äpplet. 5. Öka poängen med ormens svanslängd. 6. Göra en fördröjning genom anrop av Thread.sleep(gameLoopDelayMillis)