TDDC74 Laboration 05 - Ett objektorienterat spel 1 Översikt I den här laborationen kommer ni att bekanta er med: Promptar där användaren kan mata in kommandon. Grundläggande objektorienterad programmering. Hur man kan gå tillväga när man utvecklar ett större projekt. En praktisk datastruktur. 2 Värt att veta Denna laboration innehåller ett lite större projekt. Ni kommer att få använda Rackets inbyggda objektsystem, och ni kan ha god hjälp av dokumentationen på Rackets hemsida. 1 I denna laboration får ni använda alla primitiver som finns att tillgå i Racket. Eftersom att denna laboration är lite större så kan det vara värt att nämna att ni bör testa era skapade objekt och deras procedurer var för sig. Ni ska alltså låta bli att koda enligt bigbang-metoden. Det är mycket att läsa, men labben görs steg för steg. Bli inte avskräckt av antalet sidor. 1 docs.racket-lang.org 1
3 Projekt: Äventyrsspel I denna del av laborationsserien ska ni skapa ett litet textbaserat äventyrsspel. Ni kommer att utveckla spelet bit för bit, för att sedan sätta ihop det till en helhet. Fokus ligger på att utveckla en enklare spelmotor, skapa mallar för hur sakerna i världen, och sedan använda dessa för att ge spelet innehåll. Utvecklingsgången är ungefär 1. Hitta på historia. 2. Skapa möjligheten att ha karaktärer i spelet, och ett par karaktärer. 3. Skapa möjligheten att ha platser i spelvärlden. 4. Hantera inmatning. 5. Koppla ihop det hela till ett spel. Det är viktigt att ni genom hela processen är konsekventa med språkval (svenska/engelska) för variabler och kommentarer. Spelet kommer att vara objektorienterat och det är därför viktigt att ni har åtminstone en översiktlig koll på hur man skapar och använder objekt i Racket. Se kurshemsidan för material. 3.1 Krav Spelet måste innehålla: En sammanhängande story. Minst 4 platser. Minst 3 karaktärer. Möjlighet att nå alla platser. Minst 3 föremål. Någon karaktär som har ett specialbeteende när man ger dem en viss sak. Den ska i övrigt fungera som en vanlig karaktär. En ny plats ska göras tillgänglig när just en viss karaktär får en viss sak. Detta ska inte vara hårdkodat i ge -kommandot. 2
Instruktioner om hur man spelar och hur man vinner. Detta innebär alltså en instruktion på kommando-för-kommando-nivå. 2 Ur spelarens synvinkel ska det dessutom gå att Ta sig runt i spelvärlden och interagera med karaktärer och saker, med hjälp av ett textgränssnitt. Klara spelet. När spelet är klart, ska spelet ge något passande meddelande, och stänga av inmatningsloopen automatiskt. Notera särskilt det sista kravet. 3.2 Story Innan ni börjar skriva kod är det konstnärligt påkallat att ni bestämmer er för vad ert spel ska handla om. Det kan till exempel handla om en riddare som ska rädda en prins/prinsessa från den onda draken i en episk, interaktiv saga, ett rymdäventyr eller att lösa ett mord i ett deckarspel i noirstil. Skriv ner en liten historia och hur spelaren ska klara spelet, så att ni har något att utgå från under utvecklandet. Tänk på att inte göra det för svårt för er på det här stadiet, eller att skriva för mycket historia. Kom igång med kodandet! Med bra kod bör spelet lätt kunna vidareutvecklas om så önskas (och därmed utökas med mer historia). 3.3 Kataloger Skapa katalogen /kurs/tddc74/lab5, och kopiera filerna i TDDC74/lab/lab5/ dit. Skapa filen world-init.rkt och spara den i lab5-katalogen. Skriv sedan följande överst i filen: (require "interaction-utils.rkt") 2 D v s go west, take key, go north, use key lock, snarare än du hittar nyckeln som du behöver i skattkammaren någonstans i skogen. 3
3.4 Filstruktur I denna laboration kommer ni att behöva skapa filerna world-init.rkt character.rkt place.rkt item.rkt main.rkt player-commands.rkt Dessutom är filen interaction-utils.rkt given. character.rkt, place.rkt och item.rkt i slutet är klass-filer, alltså mallar till era objekt, som beskriver deras egenskaper och metoder/procedurer. world-init.rkt kommer att vara den fil som skapar alla karaktärer, platser och föremål. Det är alltså den fil som gör spelmotorn till ett spel. main.rkt är den fil som kommer styra spelet. Den tar in kommandon från spelaren och ser till att världen förändras utöver detta. Kommandona som spelaren kan skicka kommer definieras i player-commands.rkt. För att få tillgång till klasser och procedurer som ligger i en annan fil måste man ladda in den filen precis som i laboration 3. För att underlätta inladdningen kan man lägga alla sina filer i samma katalog (förslagsvis lab5- katalogen). Man använder sig då av require och provide. provide ger alltså tillgång till vissa definitioner i den fil som den skrivs i, medan require används för att ge tillgång till det som tillhandahålls/ provideas. Som exempel kan vi i character.rkt skriva (provide character%). Med hjälp av require kan vi då använda oss av klassen character% i andra filer: (require "character.rkt") ;Ge mig det som tillhandahålls av character.rkt (define *player* (new character% [name Me] [description "The coat looked worse for wear. Its wearer even more so."] [talk-line "You again! I have nothing to say to myself."])) 4
3.5 Klass: Karaktärer Varje karaktär i spelet ska vara ett objekt, som spelmotorn kan kommunicera med på ett förutsägbart sätt. Här definierar vi vad karaktärerna gör, och hur man kommunicerar med dem, genom att skapa en klass för karaktärsobjekt. När man sedan skapar en specifik karaktär, ska man ange vad den har för name respektive description. Namnet ska vara en symbol. Karaktärer befinner sig på platser, och de ska därför kunna hålla reda på var de själva är (platsobjekt skapar vi senare). Se till att de har denna möjligheten. Minimalt gränssnitt för klassen character% (kan behöva utökas) Metod Parametrar Beskrivning get-name - Returnera denna karaktärs namn. get-description - Returnera denna karaktärs beskrivning. get-place - Returnerar platsen karaktären är på (platsobjekt). talk - Skriver ut vad karaktären säger. move-to new-place Flyttar karaktären till new-place receive item, giver Tar mot item från giver. Här nedan följer ett skelett till klassen som ni kan utgå ifrån. (define character% ;Class name (class object% ;Inherits from object% (init-field name ;Variables without bracket are required??? ;as inarguments [???]) ;Variables in brackets are assigned ;to the value next to their name (define/public (get-name) ;/public after define???) ;makes the procedure callable from other instances (define/public (talk)???) (define/public (move-to new-place)???) ;Implement this after places% (define/public (receive item giver)???) ;Implement this after item% (super-new))) 5
För att sedan skapa ett objekt och binda det till namnet james-bond skriver ni: (define james-bond (new character% [name James-Bond] [description "Shaken, not stirred"] [talk-line "There s always something formal about the point of a pistol."])) När ni nu har ett objekt så prova att anropa detta med följande anrop. (send james-bond get-name) James-Bond ;; symbolen James-Bond returneras (send james-bond talk) ;; inget returneras, något skrivs ut "There s always something formal about the point of a pistol." Spara koden där ni skrivit klassdefinitionen i en fil med lämpligt namn. 3.6 Platser För att karaktärerna ska kunna röra sig i spelet måste det finnas platser de kan uppehålla sig på. Implementera klassen place% som ger den möjligheten. Eftersom att ni kommer spara vilka karaktärer som är på platsen i någon form av struktur som knyter ihop namn med objekt, läs kapitel 3.8.1 i labbhandledningen. I denna del ska ni inte använda den givna filen cmd_store.rkt. Däremot kommer lärdomarna från hur man använder hashtabeller att vara användbara. (Inledande) gränssnitt för place%. Metod Parametrar Beskrivning get-name - Returnerar platsens namn get-description - Returnera platsens beskrivning. add-character character Lägger till en karaktär get-character character-name Returnera karaktären med det namnet, om den finns på platsen. Annars returneras #f. delete-character character-name Ta bort karaktären från platsen. characters - Returnera en lista med alla karaktärer på platsen (som objekt) 6
> (define *java-cafe* (new place% [name java-cafe] [description "Of all the cafes in this world..."])) > (send *java-cafe* get-name) java-cafe > (send *java-cafe* get-description) "Of all the cafes in this world..." > (send *java-cafe* characters) () > (send *java-cafe* get-character james-bond) #f > (send james-bond move-to *java-cafe*) > (send james-bond get-place) (object:place%...) > (send (send james-bond get-place) get-name) java-cafe > (send *java-cafe* characters) ((object:character%...)) > (send *java-cafe* add-character james-bond) error: Character already in room: james-bond OBS! Det är viktigt att tänka på att det finns två perspektiv när man flyttar agenter mellan platser, agentens perspektiv och platsens perspektiv. Se till att ni är konsekventa här, så att man inte hamnar i oändliga loopar när man försöker flytta karaktärer. Spara koden i en fil med lämpligt namn. 3.7 Världar Ni ska nu börja koppla ihop era karaktärer med platserna. Gör detta genom att skapa en ny fil, world-init.rkt, i vilken ni laddar in era två tidigare filer med hjälp utav require. Här nedan följer ett litet exempel på hur en värld kan vara uppbyggd. (require "cmd_store.rkt") (require "interaction-utils.rkt") (require "character.rkt") (require "place.rkt") ;; Construct a couple of places 7
(define *java-cafe* (new place% [name Cafe] [description "Of all the cafes in this world..."])) (define *stairs* (new place% [name stairs] [description "It would be easy to fall here, I reminded myself. "])) (define *su07* (new place% [name su-07] [description "No one knew of this hideout of mine. I would keep it so."]) ;; We are going to create and move several characters, so let s write a ;; helper procedure... (define (make&add-character name_ desc_ talk-line_ place) (let ([new-char (new character% [name name_] [description desc_] [talk-line talk-line_])]) (send new-char move-to place) new-char)) ;; Construct the characters and add them to some place. (define *player* (make&add-character Me "The coat looked worse for wear. Its wearer even more so." "You again! I have nothing to say to myself." *java-cafe*)) (define *evil-assistant* (make&add-character evil-assistant "It was as if he had been chiseled out of a solid piece of menacing granite." "Don t let me detain you." *stairs*)) (define *good-assistant* (make&add-character 8
good-assistant "He had the kind of legs that go all the way to the floor." "Detective, what can I help you with?" *java-cafe*)) (define *mysterious-lecturer* (make&add-character mysterious-lecturer "The centuries hadn t been kind to him. Or at least, so it seemed." "That s a damn fine coat you re wearing." *java-cafe*)) Testa så att allt fungerar innan ni går vidare. 9
3.8 Att göra det till början på ett spel Det är nu dags att börja implementera en loop för att kunna hantera de olika kommandon som man vill kunna använda som spelare. Ni får i detta labbprojekt skelettet till en sådan inmatningsloop. Er uppgift blir i princip att anpassa den så att den kan hantera de olika kommandon användaren kan tänkas skriva in. 3.8.1 Spara undan kommandon Ett väldigt konkret sätt att lösa detta vore att ta skelettet (se nedan), och lägga in en stor (cond...)-sats där man räknar upp varje kommando. Det vore dock inte särskilt elegant, och gör att man knyter ihop inmatningsdelen med själva spelet på ett fult sätt (vill vi ändra storyn, eller - säg - byta till ett annat gränssnitt, måste vi bygga om rätt mycket). Istället knyter vi ihop kommandon och vad de gör i just detta spel i någon separat procedur. Det sätt vi använder här kallas hashtabeller. En hashtabell knyter namn till värden. Hur det fungerar kan man läsa mer om i Racket Guide 3 och Racket Reference 4. Datastrukturen kan visa sig vara praktisk i projektet. Att fylla i skelettet nedan är också lite av en övning i att använda hjälpen på Rackets hemsida. Så här ser cmd_store.rkt ut inuti: #lang racket(provide add-command! remove-command! valid-command? get-procedure get-valid-commands) ;; Module for storing and retrieving commands. ;; Provides ;; - add-command! : symbol x procedure -> ;; - remove-command! : symbol -> ;; - valid-command? : symbol -> bool ;; - get-procedure : symbol -> procedure (define *commands* (make-hash)) ;; Stores the procedure under name "cmd". This can 3 Se http://docs.racket-lang.org/guide/hash-tables.html 4 Se http://docs.racket-lang.org/reference/hashtables.html. 10
;; later be used in-game. cmd should be something like talk, ;; look or the like. (define (add-command! cmd procedure) your-code-goes-here) ;; Removes a binding. (define (remove-command! name) your-code-goes-here) ;; Tests if a command exists (otherwise retrieving the procedure ;; will fail). (define (valid-command? name) your-code-goes-here) ;; Retrieves a procedure. (define (get-procedure name) your-code-goes-here) ;; Returns a list of all commands (eg (jump talk...) ). (define (get-valid-commands) your-code-goes-here) När ni är klara, ska detta fungera ut som nedan: > (require "cmd_store.rkt") > (valid-command? say-hi) #f > (add-command! say-hi (lambda (args) (printf "Hi ~a!" args))) > (valid-command? say-hi) #t > (get-procedure say-hi) #<procedure> > ( (get-procedure say-hi) "Captain Vimes") Hi Captain Vimes! > (remove-binding! say-hi) > (valid-command? say-hi) #f 11
3.8.2 Att använda detta i spelloopen Genom att komplettera loopen nedan kommer ni att få en prompt där ni kan skriva in flera kommandon i rad på ett smidigt sätt. Står det» innan ett anrop är det gjort i en prompt. Vissa av procedurerna som används ligger i interactions-utils.rkt. 12
#lang racket (require "cmd_store.rkt") (require "interaction-utils.rkt") ;; Tools to store, retrieve commands etc. ;; Tools to handle user input. ;;Example on how to use add-command! (define (jump-fn args) (printf "You jump and then land.~n")) ;; When the user writes jump, the jump-fn should be used (add-command! jump jump-fn) (define (repeat-fn args) (printf "You entered: ~a~n" args)) (add-command! repeat repeat-fn) (define (interaction-loop) (printf ">> ") (enter-new-command!) (let ([name (get-command-name)] [args (get-command-arguments)]) (cond [(or (eq? name quit) (eof-object? name)) (display "Bye bye!")] [(not < your code here >) (printf "It s not possible to ~a." name) (interaction-loop)] [else < your code here > (interaction-loop)]))) Och en exempel körning (när loopen fungerar). > (interaction-loop) >> jump You jump and then land. >> repeat these words You entered: (these words) >> repeat You entered: () 13
Om allt nu är korrekt implementerat ska du kunna skriva en procedur play-game som ger lite inledande information, och startar loopen: > (play-game) Welcome to The Little Racket Adventure! >> look It s not possible to "look". >> jump You jump and then land. 3.8.3 main-fil Skapa en fil som heter main.rkt i vilken ni lägger koden för er interaction-loop. Ladda world-init.rkt överst i main.rkt so att all er initialiseringskod laddas. Flytta även ut alla kommandon och deras add-command! anrop till en ny fil, player-commands.rkt. Alla kommande kommandon ska läggas till genom anrop från player-commands.rkt. Testa så att ni kan köra spelet från main-filen och att allt fungerar. 14
4 Del 2: där saker kopplas ihop och saker blir till I den här delen kommer ni att: Koppa ihop platser med varandra så att karaktärer kan röra på sig Skapa saker som kan ligga på platser och plockas upp av karaktärer Implementera fler kommandon Förhoppningsvis ha ganska roligt 4.1 Koppla ihop platser Utöka er implementation av place% så att man kan anropa de med följande meddelanden: get-exits, get-neighbour och add-neighbour. Se tabellen nedan: Procedure Arguments Description get-name - Returns the places name get-description - Returns the places description add-character character Returns the character s place get-character character-name Returns the character with character-name if present, otherwise #f delete-character character-name Removes the specified character from this place. get-neighbour exit Returns the neighbour associated with the specified exit. If the exit is not found it returns #f add-neighbour! exit place Adds the specified neighbour to the specified exit. characters - Returns a list of all characters in this room. exits - Returns a list of the exits För att koppla ihop två platser kan en liten hjälpprocedur vara till hands: (define (connect-places! place1 exit1 place2 exit2) 15
(send place1 add-neighbour! exit1 place2) (send place2 add-neighbour! exit2 place1)) (connect-places! *java-cafe* up *stairs* down) (connect-places! *stairs* left *su07* right) Skriv ett kommando look så att man även får reda på möjliga utgångar. Tittar man i samma riktning som en utgång finns ska man se vad som ligger där. Implementera även ett kommando move så att man kan gå ut genom en exit och komma till en ny plats. När ni gjort det bör nedanstående fungera: > (play-game) Welcome to The Little Racket Adventure! Try help if this is your first time here. You are at Cafe. >> look Of all the cafes in this world... You see the following exits: down, up. You also notice: player. >> look up You look up and see that it leads to: stairs. >> move up You move up and arrive at stairs. >> look It would be easy to fall here, I reminded myself. You see the following exits: left, down. You also notice: Me. >> look down You look down and see that it leads to: Cafe. Internt (i place%) kan det vara vettigt att använda hashtabeller för att koppla ihop exit-namn med de faktiska platserna. OBS! De procedurer ni skriver, kommer att vara relativt korta. Fundera noga på vad som bör ligga i själva kommandot give och vad som bör ligga i t ex item%, character%,... Ett tips kan vara att definiera en variabel *player* som är spelaren. 4.2 Saker kommer till världen Ett spel utan föremål är inte mycket att ha. Därför ska ni nu lägga till en klass för föremål. Föremål ska ha ett namn och en beskrivning, de ska gå att plocka upp, bära med sig och kunna läggas ner. 16
När ni implementerar er nya klass, item%, kommer ni att behöva uppdatera både character% och place% med ny procedurer så att de kan hantera föremålen. Alltså vill ni lägga till procedurer i stil med: get-thing, get-things, add-item! med flera. Skapa alla nya föremål i world-init.rkt och utöka player-commands.rkt så att man till exempel kan se vad man bär på, se vilka saker som finns på platsen man är och plocka upp/släppa föremål. 17
4.2.1 item% Skapa en ny klass, i en ny fil som uppfyller åtminstone nedanstående och heter item% Minimalt gränssnitt för klassen item% Procedure Arguments Description get-name - Returns the items name get-description - Returns the items description get-place - Returns the items place move-to new-place Moves this thing from its current place or character to the new place or character. That is, it should remove itself from the place or character it previously was at and add itself to the new place (and update its place). Ni ska nu (till exempel) kunna göra följande: > (play-game) Welcome to The Little Racket Adventure! Try help if this is your first time here. >> look Welcome to the Java Cafe You see the following exits: down, up. You also notice: player. And on the ground you see: red-pen. >> take red-pen You take the red-pen. >> inventory You carry the following: red-pen. >> drop red-pen You drop the red-pen. >> inventory You do not carry anything. >> take spoon Sorry, but there is no spoon here. 18
5 Ert egna spel Efter att ha utvecklat detta skelett, skriv nu ert spel med hjälp av det. Kraven har ni i början av avsnittet. Kontrollera särskilt att ni har uppfyllt kravet om att olika karaktärer ska reagera olika på att få vissa saker. När ni lämnar in labben, bifoga även förslag på saker för labbassen att göra i spelet (så att man demonstrerar de kommandon som finns, och saker som kan hända i världen), och dessutom en komplett walkthrough på nivån lista av kommandon att skriva, för att klara spelet. 19