Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 9.1: Tekstipeli ja koodin laatu

Tästä sivusta:

Pääkysymyksiä: Saisinko vielä esimerkin ohjelmakokonaisuudesta, jossa usea luokka toimii yhteen? Miten teen ohjelmakoodistani paremmin jatkokehityskelpoisen?

Mitä käsitellään? Eräitä ohjelman suunnittelun periaatteita. Vähän ohjelman muokattavuudesta ja refaktoroinnista. Lisäharjoitusta hakurakenteista.

Mitä tehdään? Ensin luetaan, sitten ohjelmoidaan. Valmistaudutaan tiedollisesti ja henkisesti luvun 10.1 isoon ja vapaamuotoiseen ohjelmointitehtävään.

Suuntaa antava työläysarvio:? Kolme, neljä tuntia.

Pistearvo: B80.

Oheisprojektit: AdventureDraft (uusi)Adventure (uusi)
Ensinmainittua vain tutkitaan, jälkimmäistä sitten muokkaat itsekin.
../_images/person02.png

Johdanto: metsäseikkailu

Oheisprojektista 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:

../_images/adventure_console.png

Pelaa peliä hetki. Voit antaa komentoja kuten go north, go west ja rest. Komento quit lopettaa pelin.

Jos haluat, löydät pelimaailman kartan projektin sisältämästä tiedostosta forest_map.gif.

Huomaat, että peli kyllä toimii, mutta sen viihdearvo on vähäinen. Tämän pääset itse korjaamaan luvun 10.1 ohjelmointitehtävässä. Sitä ennen tässä luvussa tutkitaan valmiina annettua tekstipelitoteutusta ja parannetaan sen koodin laatua refaktoroimalla.

AdventureDraft-projektista

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 projektiin on ajaa koodia debuggerissa rivi riviltä. Tämä kaavio luokkien suhteista voi myös auttaa.

../_images/project_adventuredraft.png

Jatka vasta, kun olet tutustunut projektiin. Koodi kannattaa pitää esillä tätä lukua lukiessasi.

Tavoitteita

Mitä jos haluaisimme:

  1. lisätä peliin uuden alueen,
  2. lisätä peliin pari uutta suuntaa, joihin voi mennä (esim. käskyllä go up voisi kiivetä puuhun), ja
  3. tarjota pelaajalle graafisen käyttöliittymän, joka toimii omassa ikkunassaan kuten alla olevassa kuvassa?
../_images/adventure_gui.png

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 = new 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.

  1. 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.
  2. Area-luokkaan pitää lisätä uudet ilmentymämuuttujat upNeighbor ja downNeighbor.
  3. Jotta pelaajahahmoa voi liikuttaa näihin uusiin suuntiin, pitää Player-luokan go-metodiin lisätä pari uutta if-käskyä: if (direction == "up") jne.
  4. Jotta ylä- ja alasuuntakin tulostuvat alueen kuvauksessa mahdollisiksi liikkumasuunniksi, pitää Adventure-luokan printAreaInfo-metodiin lisätä pari uutta if-käskyä: if (area.upNeighbor.isDefined) 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") {
    destination = this.location.northNeighbor
  } else if (direction == "east") {
    destination = this.location.eastNeighbor
  } else if (direction == "south") {
    destination = this.location.southNeighbor
  } else if (direction == "west") {
    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 pelissä käytössä olevat 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")
      this.northNeighbor
    else if (direction == "east")
      this.eastNeighbor
    else if (direction == "south")
      this.southNeighbor
    else if (direction == "west")
      this.westNeighbor
    else
      None

Lisätieto

Erilaisista refaktoroinneista käytetään joskus nimiä. Esimerkiksi tässä tehdystä toimenpiteestä 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:

  1. Metodissa neighbor (yllä) on edelleen toisteinen eri suuntien luettelo. Onneksi se on sentään Area-luokan sisällä, joten suuntia lisätessä muutos tulee tähän luokkaan.
  2. Area-luokkaan täytyy yhä lisätä ilmentymämuuttuja (upNeighbor tms.) jokaista uutta suuntaa kohden.
  3. Adventure-luokan toteutus ei yksinkertaistunut lainkaan, ja uusien suuntien lisääminen edellyttää edelleen sen printAreaInfo-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)
Uutta aluetta luotaessa luodaan sille ilmentymämuuttujan neighbors arvoksi hakurakenne, jossa avaimina on merkkijonoja (suuntien nimiä) ja arvoina alueolioita.
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 hyvin luonnollinen. Ajattelemmehan itsekin esimerkiksi pelaajahahmon liikuttamisesta mieluummin "päädytään naapurialueelle suunnassa X" eikä "jos suunta on se-ja-se, otetaan se naapuri, jos taas...".

Kolmas refaktorointi: naapurien asettaminen

Emme vielä käsitelleet sitä, miten alueille asetetaan naapurit pelikarttaa muodostettaessa.

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 Adventure-luokassakin päästään aiempaa vähän helpommalla. 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 lisä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) {
    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 on mahdollista määritellä metodi, joka ottaa mielivaltaisen määrän parametreja. Tällaisenkin toteutuksen voi tehdä:

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" on muuten käytetty monen Scalan kirjastometodin 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äämiseen liittyen käsittelemättä on vielä Adventure-luokan printAreaInfo-metodi, joka näyttää alkuperäisessä projektissa 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) {
    print(" north")
  }
  if (area.eastNeighbor.isDefined) {
    print(" east")
  }
  if (area.southNeighbor.isDefined) {
    print(" south")
  }
  if (area.westNeighbor.isDefined) {
    print(" west")
  }
  println()
  println()
}

(Sivumaininta: Yllä käytetty print-metodi 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 toimimalla parannamme ohjelman koheesiota (cohesion) eli kuhunkin ohjelmakomponenttiin kuuluvien osien keskinäistä yhteenkuuluvuutta ja sitä kautta ohjelman luettavuutta ja ylläpidettävuuttä.

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 kautta tekstipelistä on saatu 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-toteutus rikkoo erästä tärkeää sovelluksen suunnittelun perusperiaatetta:

Käyttöliittymistä

Luvussa 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-projektissa on pakkaukset o1.adventure.draft.ui (käyttöliittymä) ja o1.adventure.draft (pelin sisäinen toiminta), ei tätä jakoa ole toteutettu kunnolla. Nimittäin:

  • Käyttöliittymä koostuu vain yhdestä käynnistysoliosta, joka luo Adventure-olion ja kutsuu sen run-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 luvun alkupäässä olevassa kuvassa tehtiin? 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?

Voimme muotoilla yleisemmän kysymyksen: mitä jos halutaan käyttää ohjelman sisäistä mallia jossain 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.

Projekti Adventure

Oheisprojekti Adventure on parempi pohja tekstipelin jatkokehitykselle kuin projekti AdventureDraft. Adventure-projekti poikkeaa AdventureDraft-projektista 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.
  • Projekti 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-projektiin

Nyt on hyvä vaihe ottaa esiin Adventure-projekti. Tutustu siihen itsenäisesti. Huomaa, miten se poikkeaa AdventureDraft-projektista. Yksi tapa tutustua projektiin 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.

../_images/project_adventure.png

Ohjelmointitehtävä: laajenna peliä

Täydennä projektissa Adventure annettua metsäseikkailua esineillä. 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.

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.
  • Kaukosäätimen ja paristojen alkusijainnit on merkitty Adventure-luokan koodiin.
  • Hakurakenteet sopivat tässä hyvin esineiden tallentamiseen. Käytä niitä. 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!

Palauttaminen

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Haastavanpuoleisia lisätehtäviä

Jos haluat ja ehdit, voit pohdiskella seuraavia.

  1. Yllä määriteltiin erikseen Area-luokkaan metodit neighbor, setNeighbor ja setNeighbors, ja hakurakenne neighbors 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 perspektiivin aiheeseen voi antaa Demeterin laki.
  2. Osaisitko refaktoroida Action-luokan siten, että siinä ei tarvita if-käskyä ja toistuvia Some-olioiden luomisia lainkaan? Vinkki: käytä hakurakennetta ja funktio-olioita. Onko ratkaisusi merkittävästi parempi tai huonompi kuin alkuperäinen?
  3. 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? Vinkki: käytä tehdasmetodia (luku 5.3). 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. Erityisesti piilevät riippuvuudet ovat haitallisia.
  • Lukuun liittyviä termejä sanastosivulla: refaktoroida, implisiittinen riippuvuus, abstraktio, koheesio, DRY, malli vs. käyttöliittymä.

Ryhdy jo miettimään omaa peliä!

Tekstipelin parissa jatketaan luvussa 10.1. Tuon luvun muodostaa yksi tehtävä, joka on niin iso, että koko kymppikierroksellekin on annettu aikaa kaksi viikkoa. Tässä lyhennetty versio tehtävänannosta: "Laadi ihan oma tekstipelisi muokkaamalla Adventure-projektia haluamallasi tavalla." Ideoinnin voit aloittaa vaikka heti. Myös toteuttaminen kannattaa aloittaa heti kun voit; työlästä kymppikierrosta 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!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

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, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 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.

a drop of ink
Palautusta lähetetään...