Luku 9.3: Tekstipeli ja koodin laatu
Johdanto: metsäseikkailu
Oheismoduulista AdventureDraft löytyy tekstipeli eli peli, jossa pelaaja ohjaa pelihahmoa
kirjoitetuin komennoin ja jossa pelimaailmakin on esitetty tekstimuodossa. Käynnistä
o1.adventure.draft.ui.AdventureTextUI
, jolloin peli toimii tekstikonsolissa tähän
tapaan:
Pelaa peliä hetki. Voit antaa komentoja kuten go north, go west ja rest. Komento quit lopettaa pelin.
Jos haluat, löydät pelimaailman kartan moduulin tiedostosta forest_map.gif
.
Huomaat, että peli kyllä toimii, mutta sen viihdearvo on vähäinen. Tämän pääset itse korjaamaan luvun 11.1 ohjelmointitehtävässä. Sitä ennen tässä luvussa tutkitaan valmiina annettua tekstipelitoteutusta ja parannetaan sen koodin laatua refaktoroimalla.
AdventureDraft-moduulista
Tutustu nyt itsenäisesti luokkien koodiin.
Alempana tässä luvussa oletetaan, että AdventureDraft-pelin toteutus on suunnilleen tuttu! Lue sen ohjelmakoodi läpi. Jokaista yksityiskohtaa ei tarvitse vielä ymmärtää, mutta sinun tulisi hahmottaa, mikä kunkin luokan ja metodin perusidea on.
Yksi hyvä tapa tutustua ohjelmaan on ajaa koodia debuggerissa rivi riviltä. Tämä kaavio luokkien suhteista voi myös auttaa.
Jatka vasta, kun olet tutustunut moduuliin. Koodi kannattaa pitää esillä tätä lukua lukiessasi.
Tavoitteita
Mitä jos haluaisimme
lisätä peliin uuden alueen
lisätä peliin pari uutta suuntaa, joihin voi mennä (esim. käskyllä go up voisi kiivetä puuhun)
tarjota pelaajalle graafisen käyttöliittymän, joka toimii omassa ikkunassaan kuten alla olevassa kuvassa?
Tarkastellaan kutakin kohtaa erikseen. Osoittautuu, ettei nykymuotoinen koodi taivu näihin kaikkiin tarkoituksiin kovinkaan hyvin.
Alueen lisääminen
Yksittäisen alueen lisääminen peliin onnistuu vielä hyvin. Lisätään alueen tiedot
Adventure
-luokkaan esimerkiksi näin:
val northPole = Area("The North Pole", "You find yourself at the North Pole. BRRR!")
northForest.northNeighbor = Some(northPole)
northPole.southNeighbor = Some(northForest)
Suuntien lisääminen
Suuntien — vaikkapa up ja down —- lisääminen onkin jo hankalampaa. Osoittautuu, että useita kohtia ohjelmasta pitää muuttaa.
Tietysti ainakin
Adventure
-luokan alkua on aina muutettava, kun pelin kartta muuttuu. Tuohon luokkaanhan kirjataan, mitä alueita pelissä on ja miltä alueelta pääsee mihinkin suuntaan.Area
-luokkaan pitää lisätä uudet ilmentymämuuttujatupNeighbor
jadownNeighbor
.Jotta pelaajahahmoa voi liikuttaa näihin uusiin suuntiin, pitää
Player
-luokango
-metodiin lisätä pari uuttaif
-käskyä:if direction == "up" then
jne.Jotta ylä- ja alasuuntakin tulostuvat alueen kuvauksessa mahdollisiksi liikkumasuunniksi, pitää
Adventure
-luokanprintAreaInfo
-metodiin lisätä pari uuttaif
-käskyä:if area.upNeighbor.isDefined then print(" up")
jne.
Ei hyvä. Ohjelman muokattavuuden kannalta olisi suotavaa, että muutokset ja lisäykset vaikuttavat vain harvaan eri paikkaan.
Lisäksi koodin toisteisuus sen kuin pahenee suuntia lisätessä.
Ongelmat johtuvat siitä, miten alueiden naapuruussuhteisiin liittyvä logiikka on siroteltu pitkin poikin ohjelmakoodia.
Refaktoroidaan koodia vaiheittain.
Ensimmäinen refaktorointi: parametreja Area
-luokan metodeihin
Kootaan naapuruuksien käsittely Area
-luokkaan, onhan kyse nimenomaan alueiden
ominaisuuksista. Aloitetaan kitkemällä naapuruussuuntien luetteleminen Player
-luokasta,
jonka go
-metodi näyttää nyt tältä.
def go(direction: String) =
var destination: Option[Area] = None
if direction == "north" then
destination = this.location.northNeighbor
else if direction == "east" then
destination = this.location.eastNeighbor
else if direction == "south" then
destination = this.location.southNeighbor
else if direction == "west" then
destination = this.location.westNeighbor
// ...
Emme haluaisi joutua muuttamaan Player
-luokkaa pelkän suunnanlisäyksen vuoksi. Niinpä
siellä ei saisi olla sellaista koodia, jossa luetellaan kaikki ilmansuunnat.
Pääsemme kyllä eroon suuntien mainitsemisesta. Sen sijaan, että kysyisimme alueoliolta sen "pohjoisnaapuria", "itänaapuria" jne., kysytään siltä "naapuria suunnassa X":
def go(direction: String) =
val destination = this.location.neighbor(direction)
// ...
Koodi lyheni, ja toisto sekä maininnat yksittäisistä suunnista hävisivät. Metodia go
ei
enää tarvitse muuttaa, jos peliin lisätään suuntia. Hienoa!
Nyt tietysti tarvittaisiin Area
-luokkaan tuollainen metodi nimeltä neighbor
. Tässä
ensin sivistymätön toteutus:
class Area(var name: String, var description: String):
private var northNeighbor: Option[Area] = None
private var eastNeighbor: Option[Area] = None
private var southNeighbor: Option[Area] = None
private var westNeighbor: Option[Area] = None
def neighbor(direction: String) =
if direction == "north" then
this.northNeighbor
else if direction == "east" then
this.eastNeighbor
else if direction == "south" then
this.southNeighbor
else if direction == "west" then
this.westNeighbor
else
None
Lisätieto
Erilaisista refaktoroinneista käytetään joskus nimiä; esimerkiksi tässä tehdystä voidaan käyttää nimeä parameterize method. Jos aihe kiinnostaa, löydät internetistä pitkän listan erilaisia nimettyjä refaktorointeja.
Tämä toteutus on parempi kuin alkuperäinen, mutta ei ihan kelpaa meille sekään. Valittamisen varaa nimittäin jää. Erityisesti:
Metodissa
neighbor
(yllä) on edelleen toisteinen eri suuntien luettelo. Onneksi se on sentäänArea
-luokan sisällä, joten suuntia lisätessä muutos tulee tähän luokkaan.Area
-luokkaan täytyy yhä lisätä ilmentymämuuttuja (upNeighbor
tms.) jokaiselle uudelle suunnalle.Adventure
-luokan toteutus ei yksinkertaistunut lainkaan, ja uusien suuntien lisääminen edellyttää edelleen senprintAreaInfo
-metodin muuttamista.
Kahdesta ensimmäisestä valituksesta pääsemme eroon jo tutuksi tulleella konstilla.
Toinen refaktorointi: toisto pois hakurakenteella
Yllä määritellyn neighbor
-metodin tehtävänä on hakea suuntaa (merkkijonoa) vastaava
Area
-olio. Kuulostaa hakurakenteelta?
import scala.collection.mutable.Map
class Area(var name: String, var description: String):
private val neighbors = Map[String, Area]()
def neighbor(direction: String) = this.neighbors.get(direction)
get
-metodilla saadaan Option[Area]
-tyyppinen arvo, joten
neighbor
-metodi toimii juuri niin kuin haluttiinkin.
Tämä toteutus on yksinkertaisempi ja helpommin muokattava. Luettelot ilmansuunnista
poistuivat koodista. neighbor
-metodi toimii yhtä hyvin mille tahansa merkkijonolle,
jota käytetään suunnan nimenä.
Abstraktioista
Äskeiset refaktoroinnit perustuvat abstraktioon: ei käsitellä yksittäistapauksia vaan yleinen tapaus. Tämä abstraktio on luonnollinen. Ajattelemmehan itsekin esimerkiksi pelaajahahmon liikuttamisesta mieluummin "päädytään naapurialueelle suunnassa X" eikä "jos suunta on pohjoinen, otetaan se naapuri, ja jos taas itä, jne. jne.".
Kolmas refaktorointi: naapurien asettaminen
Emme vielä käsitelleet sitä, miten alueille asetetaan naapurit, kun pelikartta muodostetaan.
Alkuperäisessä Adventure
-luokassa on tällaista koodia:
middle.northNeighbor = Some(northForest)
middle.eastNeighbor = Some(tangle)
middle.southNeighbor = Some(southForest)
middle.westNeighbor = Some(clearing)
northForest.eastNeighbor = Some(tangle)
northForest.southNeighbor = Some(middle)
northForest.westNeighbor = Some(clearing)
// jne.
Tuo koodi ei toimi yhteen uusitun Area
-luokkamme kanssa, josta northNeighbor
ja
kumppanit on poistettu. Määritellään uusittuun Area
-luokkaan metodi, joka lisää
hakurakenteeseen naapuruussuhdetta kuvaavan avain–arvo-parin:
def setNeighbor(direction: String, neighbor: Area) =
this.neighbors += direction -> neighbor
Nyt pääsemme vähän helpommalla Adventure
-luokassakin. Voimme kutsua setNeighbor
-metodia ja jättää Option
-kääreet pois:
middle.setNeighbor("north", northForest)
middle.setNeighbor("east", tangle)
middle.setNeighbor("south", southForest)
middle.setNeighbor("west", clearing)
northForest.setNeighbor("east", tangle)
northForest.setNeighbor("south", middle)
northForest.setNeighbor("west", clearing)
// jne.
Huomaa: Uusia suuntia voi lisätä peliin jopa muuttamatta Area
-luokkaa. Riittää, kun
kutsumme setNeighbor
-metodia uudenlaisella merkkijonoparametrilla.
Miten lisäisit peliin puussa sijaitsevan mökin, johon voi kiivetä? (Vastaus jääköön vapaaehtoiseksi harjoitustehtäväksi.)
Yllä esitetty toteutus naapurien asettamiseksi on jo hyvä, mutta namutellaan vielä vähän. Koska pelin kartta muodostetaan kerralla, olisi kiva lisätä useita naapureita yhdellä metodikutsulla. Koko kartan voisi määritellä vaikkapa näin:
middle .setNeighbors(Vector("north" -> northForest, "east" -> tangle, "south" -> southForest, "west" -> clearing ))
northForest.setNeighbors(Vector( "east" -> tangle, "south" -> middle, "west" -> clearing ))
southForest.setNeighbors(Vector("north" -> middle, "east" -> tangle, "south" -> southForest, "west" -> clearing ))
clearing .setNeighbors(Vector("north" -> northForest, "east" -> middle, "south" -> southForest, "west" -> northForest))
tangle .setNeighbors(Vector("north" -> northForest, "east" -> home, "south" -> southForest, "west" -> northForest))
home .setNeighbors(Vector( "west" -> tangle ))
Oletetaan, että alueolioille on määritelty
setNeighbors
-niminen metodi, ...
... jolle voi antaa parametriksi vektorillisen avain–arvo-pareja, jotka kuvaavat alueelle asetettavia ulosmenoreittejä.
Käytetään vielä välilyöntejä hieman poikkeavasti, niin koodista tulee oikein siisti. Tyylisäännöt on tehty sivistyneesti rikottaviksi.
Tuokin järjestyy, kun määritellään Area
-luokkaan metodi setNeighbors
:
def setNeighbors(exits: Vector[(String, Area)]) =
for exit <- exits do
this.neighbors += exit
Huomaa parametrin tyyppi kaikkine sulkeineen:
vektori, jonka alkioina on String
- ja Area
-olioiden muodostamia pareja.
Metodi yksinkertaisesti käy läpi kunkin parin
ja lisää sen alueolion neighbors
-hakurakenteeseen.
Vaihtoehtoisia toteutustapoja
Mainittakoon lisäkikkana, että muuttuvatilaisilla hakurakenteilla
on myös metodi ++=
, joka lisää kokoelmallisen avain–arvo-pareja
kerralla. setNeighbors
-metodin saa toteutettua myös näin:
def setNeighbors(exits: Vector[(String, Area)]) =
this.neighbors ++= exits
Ja lisälisäkikkana, että tähtimerkkiä *
käyttäen voi määritellä
metodin, joka ottaa mielivaltaisen määrän parametreja. Tämäkin
toimii:
def setNeighbors(exits: (String, Area)*) =
this.neighbors ++= exits
exits
on tässä jälkimmäisessä toteutuksessa säiliö, jossa on
kaikki annetut (String, Area)
-tyyppiset parametriarvot. Tässä
ratkaisumallissa myös Adventure
-luokan kartanmäärityskoodi
yksinkertaistuu hieman lisää:
middle .setNeighbors("north" -> nForest, "east" -> tangle, "south" -> sForest, "west" -> clearing)
nForest .setNeighbors( "east" -> tangle, "south" -> middle, "west" -> clearing)
sForest .setNeighbors("north" -> middle, "east" -> tangle, "south" -> sForest, "west" -> clearing)
clearing.setNeighbors("north" -> nForest, "east" -> middle, "south" -> sForest, "west" -> nForest )
tangle .setNeighbors("north" -> nForest, "east" -> home, "south" -> sForest, "west" -> nForest )
home .setNeighbors( "west" -> tangle )
Samaa "tähtikikkaa" (virallisemmin variable arguments eli
varargs) on muuten käytetty monen valmiin Scala-työkalun
toteutuksessa. Esimerkiksi se, että voit luoda uuden hakurakenteen
käskyllä Map(...)
antaen kuinka monta parametria vain, perustuu
juuri tähän.
Neljäs refaktorointi: määritellään metodi datan luokse
Suuntien lisäämisen osalta käsittelemättä on vielä Adventure
-luokan printAreaInfo
-metodi. Se näyttää alkuperäisessä ohjelmassa tältä:
def printAreaInfo() =
val area = this.player.location
println("\n\n" + area.name)
println("-" * area.name.length)
println(area.description)
print("\nExits available:")
if area.northNeighbor.isDefined then
print(" north")
if area.eastNeighbor.isDefined then
print(" east")
if area.southNeighbor.isDefined then
print(" south")
if area.westNeighbor.isDefined then
print(" west")
println()
println()
(Sivumaininta: Yllä käytetty print
-funktio on samanlainen
kuin println
, paitsi ettei se vaihda riviä tulostettuaan merkit.
Uloskäynnit siis tulostuvat yhdelle riville peräkkäin.)
printAreaInfo
-metodin ongelmaksi olemme jo todenneet, että siinä luetellaan yksittäisiä
ilmansuuntia. Eikä ratkaisu enää edes toimi: uudessa versiossammehan naapureita kuvaavia
erillisiä ilmentymämuuttujia ei ole.
Lisäksi arveluttaa se, että tämä Adventure
-luokan metodi käsittelee lähes yksinomaan
sellaista dataa, joka on tallennettu toisaalle (Area
-olioon). Olio-ohjelmoinnissahan
ajatuksena olisi, että pyritään ensisijaisesti määrittelemään toiminnot niiden
käsittelemän datan yhteyteen. Näin parannamme ohjelman koheesiota (cohesion) eli
kuhunkin ohjelmakomponenttiin kuuluvien osien keskinäistä yhteenkuuluvuutta, ja siten
ohjelmasta tulee luettavampi ja ylläpidettävämpi.
Mainitut ongelmat ratkeavat siirtämällä printAreaInfo
Adventure
-luokasta Area
-luokkaan, jossa sen käsittelemä datakin on. Samalla voimme muokata sen hyödyntämään
neighbors
-ilmentymämuuttujaan tallennettua hakurakennetta:
def printAreaInfo() =
println("\n\n" + this.name)
println(this.name.replaceAll(".", "-"))
println(this.description)
println("\nExits available: " + this.neighbors.keys.mkString(" ") + "\n\n")
Mitä olemme saaneet aikaan?
Yllä kuvattujen muutosten myötä tekstipeli on jatkokehityskelpoisempi. Samalla olet toivottavasti oppinut jotakin ohjelmien refaktoroinnista. Jotain on kuitenkin vielä tekemättä.
Luvun alussa mainittiin myös, että voisimme haluta tekstipelille omassa ikkunassaan toimivan graafisen käyttöliittymän. Tämä on nykyisellään vaikeaa, sillä AdventureDraft rikkoo erästä tärkeää sovelluksen suunnittelun perusperiaatetta:
Käyttöliittymistä
Luvuissa 1.2 ja 2.7 totesimme, että sovellusohjelmien pääosia ovat aihealueen malli ja käyttöliittymä. Tällainen jako on toteutettu useisiin kurssin varrella näkemiisi ohjelmiin.
Vaikka myös AdventureDraft-ohjelmassa on pakkaukset o1.adventure.draft.ui
(käyttöliittymä) ja o1.adventure.draft
(pelin sisäinen toiminta), ei tuota
jakoa ole toteutettu kunnolla. Nimittäin:
Käyttöliittymä koostuu vain yhdestä käynnistysoliosta, joka luo
Adventure
-olion ja kutsuu senrun
-metodia. Tämä olio ei huolehdi lainkaan käyttäjän kanssa kommunikoinnista eli tulostamisesta ja syötteen lukemisesta.Toisaalta ohjelman sisäiseen malliin kuuluvat pelimaailmaa mallintavat luokat tulostelevat ruudulle tietoja milloin mistäkin metodista. Syötteen lukeminen tapahtuu
Adventure
-luokan metodissa, vaikka tämäkin luokka kuuluu ohjelman sisäiseen käsitemalliin.
Mitä jos haluamme korvata tekstikäyttöliittymän GUI-ikkunalla, joka näyttää pelin tilan tekstialuekomponenteissa kuten tämän luvun alkupään kuvassa? Muutoksia pitäisi tehdä sinne tänne ja korvata tulostus- ja syötteenlukukäskyjä GUI-koodilla. Ja entä jos haluamme tarjota samalle pelille kaksi vaihtoehtoista käyttöliittymää — graafinen ja tekstikonsolissa toimiva — joista käyttäjä voi valita kumman tahansa käynnistettäväksi? Pitäisikö tehdä kokonaan erilliset versiot pelistä, joissa iso osa koodista toistuu molemmissa?
Muotoillaan kysymys vielä yleisemmin: mitä jos halutaan käyttää ohjelman sisäistä mallia jossakin muussa käyttöliittymässä tai peräti toisessa sovelluksessa?
Mallin erottaminen käyttöliittymästä
Hyvin tehdyssä sovelluksessa sisäinen malli on mahdollisimman riippumaton käyttöliittymästä (ja mahdollisesta muusta mallia hyödyntävästä ohjelmakoodista kuten testiohjelmasta). Malliin kuuluvat ohjelman osat eivät kommunikoi käyttäjän kanssa. Malliin kuuluvista luokista ei esimerkiksi tulosteta näytölle, lueta näppäimistöä, avata ikkunoita tai reagoida nappulanpainalluksiin. Käyttöliittymää voi muuttaa tai vaihtaa koskematta malliin.
Käyttöliittymä esittää mallin jollain tavoin ohjelman käyttäjälle (esim. tekstitulosteet pelimaailmasta) ja ottaa jollain tavoin käyttäjältä vastaan tietoja, joiden perusteella mallia käsitellään (esim. hahmonohjauskomennot). Se kutsuu malliin kuuluvien olioiden metodeita käyttäjän antamien syötteiden mukaisesti. Niinpä käyttöliittymän täytyy tietää tarkasti, millaista mallia sen kautta käytetään, eikä se voi olla mallista riippumaton.
Adventure-moduuli
Oheismoduuli Adventure on parempi pohja tekstipelin jatkokehitykselle kuin AdventureDraft. Adventure poikkeaa AdventureDraftistä näin:
Siihen on tehty yllä esitetyt refaktoroinnit.
Lisäksi malli on erotettu käyttöliittymästä:
Kaikki tulostaminen ja näppäimistön lukeminen tapahtuu luokassa
AdventureTextUI
.Sisäiseen malliin kuuluvat luokat eivät tulosta mitään vaan vain palauttavat merkkijonoja, jotka kuvaavat alueita, komentojen seurauksia, tapahtumia jne. Käyttöliittymä valitsee, miten nämä merkkijonot esitetään käyttäjälle. (Oletuksena säilytämme, että mikä tahansa tämän pelin käyttöliittymä perustuu tavalla tai toisella tekstiin.)
Tarjolla on myös graafinen käyttöliittymä, jonka voi käynnistää oliosta
AdventureGUI
.Moduuli sisältää esineitä kuvaavan pikkuluokan
Item
, ja usean luokan Scaladoc-dokumentaatiossa on esineisiin liittyviä metodeita, joita ei vielä löydy koodista. Näiden merkitys selviää seuraavassa ohjelmointitehtävässä.
Tutustu nyt Adventure-ohjelmaan
Nyt on hyvä vaihe ottaa esiin Adventure-moduuli. Tutustu siihen itsenäisesti. Totea, miten se poikkeaa AdventureDraft-moduulista. Yksi tapa tutustua ohjelmaan on ajaa koodia debuggerissa rivi riviltä.
Älä unohda tutustua myös Action
-luokkaan, vaikka sitä ei tämän
luvun tekstissä olekaan suuremmin käsitelty.
Ohjelmointitehtävä: laajenna peliä
Täydennä moduulissa Adventure annettua metsäseikkailua esineillä. Tee se hakurakenteita käyttäen.
Siis:
Alueessa (
Area
) pitää voida olla esineitä. Esineitä kuvaamaan on valmiina annettu luokka nimeltäItem
, mutta muut luokat eivät vielä hyödynnä sitä.Alueen sisältämät esineet pitää tulostaa alueen muun kuvauksen yhteydessä, kun pelaaja on kyseisellä alueella.
Pelaajan pitää pystyä poimimaan esineitä komennolla get itemname ja tiputtamaan niitä komennolla drop itemname.
Pelaajan pitää pystyä tutkimaan poimimiaan esineitä komennolla examine itemname.
Komennolla inventory pelaajan pitää pystyä "tekemään inventaario" esineistä, jotka hänellä on mukanaan.
Pelin voittoehtoa pitää muuttaa hieman mutkikkaammaksi. Ei riitäkään, että pelaaja löytää tiensä kotiin, vaan pelaajan pitää ensin saada käsiinsä kaukosäädin ja paristoja ja vasta sitten päätyä kotiin.
Hakurakenteet (
Map[String, Item]
) sopivat tässä hyvin esineiden tallentamiseen. Käytä niitä. Älä käytä hakurakenteen sijaan esim. vektoria tai puskuria!
Alempana on esimerkki siitä, miten pelin pitäisi toimia, kun muutokset on tehty.
Ohjeita ja vinkkejä
Annettuihin luokkiin täytyy lisätä uusia metodeita uusien toimintojen toteuttamiseksi. Näiden metodien kuvaukset löytyvät annetusta Scaladoc-dokumentaatiosta.
Osa muutoksista tulee
Action
-luokkaan, jota ei ole tässä luvussa tarkemmin käsitelty.
Kaukosäätimen ja paristojen alkusijainnit on merkitty
Adventure
-luokan koodiin.Pelin esineillä on yksiselitteiset nimet, eli kahdella esineellä ei voi olla samaa nimeä.
Lisää ensin esineet pelin alueisiin. Työstä sen jälkeen komentoja, joilla esineitä voi käsitellä.
Pidä huolta siitä, että ohjelman tuloste vastaa täsmälleen alla annettua esimerkkiä ja dokumentaatiota, tai A+ suuttuu.
Esimerkkituloste
You are lost in the woods. Find your way back home. Better hurry, 'cause Scalatut elämät is on real soon now. And you can't miss Scalkkarit, right? Forest ------ You are somewhere in the forest. There are a lot of trees here. Birds are singing. Exits available: west north south east Command: go east You go east. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go east You go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: inventory You are empty-handed. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go east You can't go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go west You go west. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: get remote There is no remote here to pick up. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go west You go west. Forest ------ You are somewhere in the forest. A tangle of bushes blocks further passage north. Birds are singing. Exits available: west south east Command: go west You go west. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. You see here: battery Exits available: west north south east Command: examine battery If you want to examine something, you need to pick it up first. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. You see here: battery Exits available: west north south east Command: get battery You pick up the battery. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: inventory You are carrying: battery Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: examine battery You look closely at the battery. It's a small battery cell. Looks new. Forest Clearing --------------- You are at a small clearing in the middle of forest. Nearly invisible, twisted paths lead in many directions. Exits available: west north south east Command: go south You go south. Forest ------ The forest just goes on and on. You see here: remote Exits available: west north south east Command: get remote You pick up the remote. Forest ------ The forest just goes on and on. Exits available: west north south east Command: examine remote You look closely at the remote. It's the remote control for your TV. What it was doing in the forest, you have no idea. Problem is, there's no battery. Forest ------ The forest just goes on and on. Exits available: west north south east Command: inventory You are carrying: remote battery Forest ------ The forest just goes on and on. Exits available: west north south east Command: go east You go east. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: drop remot You don't have that! Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: drop remote You drop the remote. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: drop remote You don't have that! Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: go east You go east. Home ---- Home sweet home! Now the only thing you need is a working remote control. Exits available: west Command: go west You go west. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: get remot There is no remot here to pick up. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. You see here: remote Exits available: west north south east Command: get remote You pick up the remote. Tangle of Bushes ---------------- You are in a dense tangle of bushes. It's hard to see exactly where you're going. Exits available: west north south east Command: go east You go east. Home at last... and phew, just in time! Well done!
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vaikeanpuoleisia lisätehtäviä
Jos haluat ja ehdit, voit pohdiskella seuraavia.
Yllä määriteltiin erikseen
Area
-luokkaan metoditneighbor
,setNeighbor
jasetNeighbors
, ja hakurakenneneighbors
oli yksityinen. Olisimme myös voineet tehdä hakurakenteesta julkisen. Tällöin mainittuja metodeita ei olisi välttämättä tarvittu. (Miksei?) Onko tällä ratkaisulla jotain haittapuolia? Erään näkökulman aiheeseen antaa Demeterin laki.Osaisitko refaktoroida
Action
-luokan niin, ettei siinä tarvitaif
/match
-käskyä ja toistuviaSome
-olioiden luomisia lainkaan? Vinkki: käytä hakurakennetta ja funktio-olioita. Onko ratkaisusi merkittävästi parempi tai huonompi kuin alkuperäinen?Entä osaisitko refaktoroida tekstipelin niin, että
Action
-luokka olisikin abstrakti luokka tai piirreluokka, ja erilaisia toimintoja kuvaisivat sen alakäsitteiksi määritellyt konkreettiset luokat? MissäAction
-olioiden luomisen voisi tällöin hoitaa? Onko ratkaisu merkittävästi parempi tai huonompi kuin alkuperäinen?
Näihin(kään) ei ole yhtä ainoaa oikeaa ratkaisua.
Yhteenvetoa
Tapa, jolla ohjelma on laadittu, vaikuttaa siihen, kuinka helppoa ohjelmaan on tehdä lisäyksiä ja muita muutoksia.
Ohjelmakoodin laatuominaisuuksia kuten muokattavuutta ja luettavuutta voidaan parantaa refaktoroimalla.
Muokattavuus paranee usein vähentämällä ohjelman osien välisiä riippuvuuksia. Piilevät riippuvuudet ovat erityisen haitallisia.
Lukuun liittyviä termejä sanastosivulla: refaktoroida, piilevä riippuvuus, abstraktio, koheesio, DRY, malli vs. käyttöliittymä.
Ryhdy jo miettimään omaa peliä!
Tekstipelin parissa jatketaan luvussa 11.1. Tuon luvun muodostaa yksi tehtävä, joka on niin iso, että koko tuolle kierroksellekin on annettu aikaa kaksi viikkoa. Tässä lyhennetty versio tehtävänannosta: "Laadi ihan oma tekstipelisi muokkaamalla Adventure-ohjelmaa haluamallasi tavalla." Ideoinnin voit aloittaa vaikka heti. Suosittelemme tässä parityötä myös niille, jotka tekivät aiemmat tehtävät yksin! Aloita myös toteuttaminen heti kun voit; työlästä 11. kierrosta ei sovi jättää viimeiseen iltaan.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
Tämän luvun inspiraationa ovat olleet vanhat Infocomin seikkailupelit sekä David Barnesin ja Michael Köllingin laatima oppimateriaali.
Kullekin uudelle alueelle luodaan ilmentymämuuttujan
neighbors
arvoksi hakurakenne, jossa avaimina on merkkijonoja (suuntien nimiä) ja arvoina alueolioita.