Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 7.4: Raittiuspeli
Tästä sivusta:
Pääkysymyksiä: Miten pärjään työstäessäni laajempaa annettua koodia, jossa usea luokka riippuu toisistaan?
Mitä käsitellään? Luvun muodostaa käytännössä yksi ohjelmointitehtävä. Se tarjoaa treeniä mm. periytymisestä ja kokoelmametodeista.
Mitä tehdään? Tutustutaan annettuun koodiin ja ohjelmoidaan.
Suuntaa antava työläysarvio:? Pari, kolme tuntia.
Pistearvo: B60.
Oheismoduulit: Viinaharava (uusi).
Viinaharava-peli
Paikallinen raittiusseura on tilannut veden juomiseen kannustavan pelin. Tuloksena on jo syntynyt melkein valmis raittiuspeli nimeltä Viinaharava, jonka täydentäminen jää nyt sinun huoleksesi.
Viinaharava-pelissä on sijoitettu pieniä juomalaseja suorakaiteen muotoiseen ruudukkoon. Suurimmassa osassa on vettä, mutta muutamassa on tiukkaa alkoholijuomaa. Pelaajan tehtävänä on juoda kaikki vedet haksahtamatta viinaan. Virtuaalijuonti käy näpäyttämällä juotavaksi valittua lasia. Pelaajan tehtävää helpottaa se, että kunkin juodun lasin pohjasta löytyy vihje: tieto siitä, kuinka monta viinaa kyseisen lasin viereisissä ruuduissa on. Peli päättyy, kun kaikki vedet tai yksikin viina on juotu.
Tehtävänanto
Osittain toimiva toteutus pelille löytyy Viinaharava-moduulista. Se on esitelty tarkemmin alempana tällä sivulla.
Tutustu moduuliin ja täydennä puuttuvat osat. Voit edetä esimerkiksi näin:
- Aja Viinaharava-peli käynnistysoliosta
Viinaharava
lähtien. Huomaat: pelikenttä tulee näkyviin, mutta peli ei toimi. - Tutustu luokkaan
o1.Grid
, jota Viinaharavan toteutuksessa on käytetty apuna. Siitä on lisätietoja alla. - Tutustu pakkauksen
o1.viinaharava
luokkiin. Aloita pakkauksen esittelystä alempana tällä sivulla. Jatka scaladoceihin ja ohjelmakoodiin. - Kun ymmärrät annetun ohjelman, täydennä sen puuttuvat osat. Lisää ohjeita ja vinkkejä on tarjolla alempana tällä sivulla.
Työkaluja ruudukon mallintamiseen
Muistat matopelin luvusta 6.3. Siinä madon osat ja ruoka sijaitsivat eri kohdissa
ruudukkomaista pelikenttää, minkä kirjasimme käyttäen GridPos
-olioita. Kukin GridPos
muodostui kahdesta kokonaisluvusta x
ja y
, jotka vastaavat tiettyä "ruutua".
Viinaharavassa on samaa kuin matopelissä: siinäkin pelimaailman muodostaa eräänlainen
ruudukko. Voimme käyttää lasien sijaintien kuvaamiseen GridPos
-luokkaa.
(Tässä tehtävässä sinun ei tarvitse lainkaan käsitellä pikseleita tai grafiikkaa. Annettu
käyttöliittymä huolehtii niistä. Voit keskittyä pelin sisäiseen maailmaan, jossa sijainteja
käsitellään ruutujen numeroina eli GridPos
-olioina.)
Matopelissä ruudukko oli "harva": ruudukkoon sijoitettuja asioita (madon osia, ruokaa)
oli vain vähän suhteessa ruudukon kokoon. Mallinsimme pelin kirjaamalla vain sen, missä
GridPos
-koordinaateissa on jotain ja muualla oli sitten tyhjää.
Tällä kertaa teemme toisin ja mallinnamme Viinaharavan pelilautaa "tiheänä" ruudukkona: kirjaamme jokaisesta ruudukon sijainnista erikseen, millainen lasi siinä on. Onko se vesi? Onko lasi jo tyhjennetty? Montako vaarallista naapuria sillä on?
Ruudukon kuvaaminen tiheästi käy kätevämmin, kun otamme käyttöön lisäapuvälineen, luokan
Grid
.
Luokka Grid
Kurssin peruskirjastossa O1Library on luokka o1.Grid
. Kukin Grid
-olio edustaa
ruudukkoa, jossa jonkinlaisia keskenään samankokoisia elementtejä (esim. laseja) on
asemoitu suorakaiteen muotoon xy-koordinaatistoon. Grid
tarjoaa metodeita tällaisen
ruudukon käsittelemiseen. Sen avulla voi esimerkiksi poimia koordinaattiparin perusteella
tietyn ruudun (elementAt
tai apply
), etsiä kaikki tietyn ruudun naapuriruudut
(neighbors
) ja selvittää ruudukon koon (width
, height
ja size
).
Grid
-luokka on abstrakti. Siitä ei voi luoda ilmentymiä noin vain käskyllä new Grid
vaan vain aliluokkiensa kautta. Tämä abstrakti luokka on tarkoitettu sopimaan erilaisiin
ohjelmiin, joissa on tarpeen käsitellä ruudukkoja ja ruutujen koordinaatteja: Grid
ei
ota kantaa siihen, millainen yksittäinen ruutu on, vaan se jää aliluokkien määriteltäväksi.
Viinaharava on eräs Grid
-luokan käyttötapaus: pelilauta on ruudukko, joka muodostuu
laseja kuvaavista olioista. Myöhemmissä tehtävissä tulemme käyttämään Grid
iä myös
muunsisältöisten ruudukkojen kuvaamiseen.
Viinaharava-moduulin osat
Viinaharava-moduuli muodostuu kahdesta pakkauksesta. Käyttöliittymäpakkaukseen
o1.viinaharava.gui
sinun ei tarvitse tarkemmin tutustua, eikä sitä tässä tarkemmin
esitellä. Riittää kun saat käyttöliittymän käyntiin. Tehtävässä muokataan pelin
varsinaista sisältöä, joka on pakkauksessa o1.viinaharava
.
Moduulin kaksi keskeistä luokkaa ovat:
Glass
: tämän luokan ilmentymät ovat yksittäisiä laseja, joista pelilauta koostuu.GameBoard
-olio puolestaan edustaa pelilautaa kokonaisuutena. Pelilauta on eräänlainen ruudukko;GameBoard
onGrid
in aliluokka.
Tässä kokoava kaavio mainituista luokista ja niiden välisistä suhteista:
Kaavion alaosa tahtoo sanoa, että kuhunkin pelilautaan liittyy useita laseja, joista
kullakin on omat koordinaatit kyseisellä laudalla. Tietyn GameBoard
-olion tietty
Glass
-olio voidaan poimia GridPos
-olion perusteella.
Glass
-luokka ja sen puuttuvat metodit
Yksittäinen lasi voi olla joko täynnä tai tyhjä. Sen sisältö voi olla joko vettä tai viinaa. Kukin lasiolio myös pitää kirjaa vaarallisuudestaan eli siitä, montako viinaa vaanii viereisissä ruuduissa (0–8 kpl; vinosuunnat lasketaan mukaan).
Lasiolioille on määritelty ilmentymämuuttujat, joissa voi pitää kirjaa täyttöasteesta,
sisällön tyypistä ja vaarallisuudesta. Lisäksi kukin Glass
-olio "tietää", millä
pelilaudalla se on ja missä koordinaateissa.
Lasi on luotaessa täynnä vettä. Glass
-luokka tarjoaa metodeita, joilla lasin tilaa voi
päivittää. Päivityksiä on kahdenlaisia:
- Lasin voi tyhjentää. Metodia
empty
kutsutaan, kun käyttäjä näpäyttää hiirellä lasin kuvaa käyttöliittymässä. - Lasin voi täyttää viinalla (
pourBooze
). Tämä myös kasvattaa viereisten ruutujen vaarallisuutta.pourBooze
-metodia kutsutaan ennen pelin alkua, kun lauta viinoitetaan. (Käyttöliittymässä on myös mahdollista testimielessä kaataa lisää viinoja pelin aikana.)
pourBooze
-metodi on kuitenkin toteuttamatta annetussa ohjelmakoodissa. Samoin puuttuu
neighbors
-metodi, jolla tulisi voida selvittää lasin viereiset lasit.
GameBoard
-luokka ja sen puuttuvat metodit
Tässä alkua GameBoard
-luokalle:
class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid(width, height) {
// ...
Grid
-oliota luotaessa tarvitaan leveys ja korkeus.
Nämä kaksi parametriarvoa välitetään yliluokan ohjelmakoodin
käsiteltäviksi.Alkuriville tarvitaan vielä pieni lisäys ennen kuin määrittely toimii. Yliluokka Grid
vaatii nimittäin konstruktoriparametrien lisäksi tyyppiparametrin. Samaan tapaan kuin
vaikkapa tuttua luokkaa Buffer
käyttäessä täsmennämme hakasulkeissa, mikä puskurin
alkoiden tyyppi on, myös Grid
-luokkaa käyttäessämme meidän tulee kirjata millaisista
elementeistä ruudukko koostuu.
class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid[Glass](width, height) {
// ...
GameBoard
-olio on sellainen Grid
, jossa ruudukon joka
kohdassa on Glass
-olio:Kuten kokeilemalla näit, annettu Viinaharava-toteutus jo luo laudan täyteen vesilaseja. Katsotaan annettua koodia hieman pidemmälle, niin näemme, mikä osa tuosta vastaa.
class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid[Glass](width, height) {
def initialElements = {
val allLocations = (0 until this.size).map( n => GridPos(n % this.width, n / this.width) )
allLocations.map( loc => new Glass(this, loc) )
}
this.placeBoozeAtRandom(boozeCount)
Grid
jättää abstraktiksi metodin, jonka tulee tuottaa luettelo kaikista
niistä elementeistä, jotka ruudukossa aluksi ovat. (Yliluokka
kutsuu tätä metodia automaattisesti, kun uusi ruudukko luodaan.)GameBoard
toteuttaa tämän metodin palauttamalla
kokoelmallisen tyhjiä Glass
-olioita. Voit tutustua tähän
toteutukseen, jos haluat; tehtävän kannalta välttämätöntä se
ei ole. Tähän metodiin ei tarvitse eikä pidä tehdä muutoksia.placeBoozeAtRandom
-metodikutsu
on osa GameBoard
-ilmentymän alustavaa koodia (eli konstruktoria).
Se suoritetaan aina, kun uusi pelilauta luodaan.Tuo placeBoozeAtRandom
-metodi on kuitenkin annettuun koodiin toteutamatta,
joten pelilaudalle ei tule viinoja. Asia täytyy korjata.
Toteutus puuttuu myös drink
-metodilta, minkä vuoksi peli ei tee mitään laseja hiirellä
klikatessa. Ja isOutOfWater
-metodilta, jota käytetään pelin päättymisen tarkistamiseen.
Alla on kolmivaiheinen ehdotus siitä, miten voit edetä.
Suositellut työvaiheet
Vaihe 1/3: vedet
Täydennä GameBoard
-luokan drink
-metodin vesilaseja käsittelevä if
-haara.
Toteuta sitten saman luokan metodi isOutOfWater
.
- Saat kaikki laudan lasit helposti
Grid
-luokasta periytyvälläallElements
-metodilla. - Sopivalla korkeamman asteen metodilla (luku 6.3) toteutus on yksinkertainen.
Kokeile peliä uudestaan. Laseja voi nyt tyhjentää mielin määrin. Kun kaikki vedet on klikattu, sovellus ilmoittaa asiasta. Viinojen tuoma jännitys puuttuu.
Vaihe 2/3: viinat laudalle
Toteuta Glass
-luokan neighbors
-metodi. Vinkki: hyödynnä olemassa olevaa metodia, niin
ratkaisusta tulee hyvin yksinkertainen.
Täydennä sen jälkeen samaan luokkaan pourBooze
-metodi. Kun olet tehnyt tämän, niin
viinaa saa laseihin ja eri lasien "vaarallisuus" nousee asianmukaisesti. Kuitenkaan pelin
toiminta ei vielä muutu, koska kyseistä metodia ei vielä kutsuta mistään.
Siirry GameBoard
-luokan yksityiseen placeBoozeAtRandom
-metodiin. Täydennä se kaatamaan,
sattumanvaraisiin laseihin viinoja niin monta kuin sen parametri määrää. Tarkoituksena on
arpoa tietty määrä viinoja pelilaudalle niin, että sijainnit vaihtelevat pelikertojen
välillä. Jokaisen arvotun sijainnin on oltava erilainen, sillä viinat eivät voi olla
päällekkäin.
Alla on kuvattu kaksi eri tapaa lähestyä ongelmaa. Voit valita niistä kumman haluat, tai voit käyttää jotakin muutakin tapaa, kunhan lopputulos toimii.
Algoritmi 1:
- Arvo satunnaisgeneraattoria käyttäen koordinaattipari.
- Selvitä, onko juuri arvotuissa koordinaateissa jo viina.
- Jos on, älä tee mitään.
- Jos ei, kaada viina kyseiseen lasiin.
- Toista kohtia 1 ja 2, kunnes laudalla on haluttu lukumäärä viinoja.
Kumpi on parempi?
Voit miettiä, kumpi viereisistä ratkaisuista vaatii tietokoneelta enemmän töitä (aikaa). Miten työmäärä riippuu laudan koosta ja viinojen lukumäärästä?
Algoritmi 2:
- Muodosta kokoelma, jossa on jokainen lasiolioista.
- Sekoita kokoelman sisältämät oliot sattumanvaraiseen järjestykseen.
(Tämän voi tehdä vaikka silmukalla itsekin, mutta helpommin se sujuu
käyttämällä
Random
-olion kätevääshuffle
-metodia, josta on esimerkki hieman alempana.) - Ota kokoelmasta haluttu määrä laseja. Kaada viina kuhunkin niistä.
Tässä esimerkki mainitusta shuffle
-metodista:
import scala.util.Randomimport scala.util.Random val lukuja = (1 to 10).toVectorlukuja: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) Random.shuffle(lukuja)res0: Vector[Int] = Vector(8, 9, 7, 4, 6, 1, 10, 2, 5, 3) Random.shuffle(lukuja)res1: Vector[Int] = Vector(8, 6, 4, 5, 9, 1, 3, 7, 2, 10)
Vaihe 3/3: viinojen juonti
Voit jälleen kokeilla ohjelmaa. Sitä pitäisi jo voida pelata, mutta viinat eivät tule näkyviin niihin osuessa. Tarkoitus olisi, että pelaajan haksahtaessa näkyviin tulisivat (eli tyhjenisivät) kaikki koko laudan viinat.
Täydennä drink
-metodin toinen haara eli viinaan osumisen tapaus. Voit hyödyntää
pelilaudan boozeGlasses
-metodia, joka palauttaa vektorissa kaikki laudan viinalasit.
Kokeile taas. Ohjelman pitäisi nyt olla varsin pelikelpoinen.
Kulaus per klikkaus, toistaiseksi
Mieleesi saattaa juolahtaa, että kun juodun veden vaarallisuus on nolla, eli naapurissa ei ole yhtään viinaa, niin samalla näpäyksellä voisi turvallisesti siemaista naapurilasitkin tyhjiksi. Tällainen lisätoiminnallisuus toteutetaankin luvussa 12.1 rekursioksi kutsutulla ohjelmointitekniikalla.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
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, 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 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 ja Juha Sorva. 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. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa 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 ovat luoneet Nikolai Denissov, Olli Kiljunen ja Nikolas Drosdek yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
GameBoard
-olio tarvitsee kolme konstruktoriparametria: laudan leveyden ruutuina, laudan korkeuden ruutuina ja viinalasien lukumäärän.