Tehtävä 5 - Sokoban

Ruotsinkielinen tehtävänanto saatavilla osoitteessa: http://www.niksula.cs.hut.fi/~alipiain/tehtavat/kierros3/tehtava3sve.html

Tehtävänantoon tulleet muutokset, 12.11. klo 11

Taustaa

Tässä tehtävässä toteutamme Scalan Swing-luokkakirjaston avulla klassisen pelin nimeltä Sokoban (tunnettu myös nimellä Boxxle). Siinä pelaaja liikkuu ylhäältä kuvatussa suorakulmaisessa sokkelossa ja työntää siellä olevia laatikoita, tavoitteena joko saada kaikki laatikot siirrettyä tiettyyn paikkaan tai (kuten meidän pelissämme on) yksinkertaisesti selviytyä itse maaliin.

Osatehtäviä on melko paljon, mutta kolmatta lukuun ottamatta ne ovat melko pieniä. Kannattaa lukea jokainen osatehtävä kokonaan ennen kuin aloittaa sen toteuttamisen. Muista myös kääntää ja testata tehtäväsi uusien ominaisuuksien lisäämisen jälkeen.

Tehtävänanto

Tämän tehtävänannon suorittamisen jälkeen osaat:

Tehtävässä tarvittavat luokat voit ladata kerralla zip-pakettina tästä: luokat.zip.

1. Osatehtävä - Kehys Sokoban

Aloitetaan laatimalla objekti Sokoban, joka perii SimpleSwingApplication-luokan. SimpleSwingApplication-luokan periminen on helpoin tapa toteuttaa yksinkertainan graafisella käyttöliittymällä varustettu ohjelma. Toteuttamalla luokan top-metodin pystytään luoda ohjelmallemme pääikkuna. top-metodin toteutuksen tulee siis olla sellainen, että metodi palauttaa MainFrame-olion. Voit katsoa apua Kierroksella 3 käyttämästäsi View.scala-luokasta.

Seuraavaksi top-metodissa:

Viimeiseksi luodaan vielä ikkunaasi valikkorivi. Swingillä rakennetut valikkorivit koostuvat kolmentyyppisistä olioista. Aluksi on kaiken kokoava valikkoriviluokka MenuBar, joka sisältää yhden tai useamman valikon eli Menu-olion. Näihin Menu-oliohin voidaan lisätä taas yksi tai useampi valikkonimike MenuItem. Luo siis ikkunallesi valikkorivi, johon asennat valikon nimeltä "Peli" ja tähän valikkoon nimikkeet "Aloita alusta" ja "Lopeta". Myöhemmissä vaiheissa valikkoihin lisätään myös toiminnot.

Tässä vaiheessa olet jo:

Muista kokeilla luokkasi ajamista!

2. Osatehtävä - Enumeraatio GridType

Tässä osatehtävässä tutustumme erikoislaatuiseen luokkaan — enumeraatioon. Enumeraatio on lueteltu tietotyyppi, joka sisältää jonkin määrättävän joukon kaikki alkiot. Esimerkiksi voitaisiin luoda viikonpäiviä kuvastava enumeraatio, joka sisältää kaikki viikonpäivät (maanantai, tiistai, keskiviikko, torstai, perjantai, lauantai, sunnuntai) ja mahdollisesti niiden operoimiseen tarvittavia metodeja. Enumeraatiossa siis määritellään valmiiksi kaikki mahdolliset luokan ilmentymän arvot. Tätä voi siis käyttää silloin, kun mahdollisia vaihtoehtoja on rajallinen määrä ja vaihtoehdot tunnetaan ennalta.

Toteuta pelikentän ruutujen kuvaamiseen tarkoitettu luokka GridType periyttämällä se Enumeration-luokasta. Kirjoita enumeraatiosi omaan tiedostoon. Katso apua tyyppien määrittämiseen luokan Enumeration dokumentaatiosta. Tässä tehtävässä tarvittavat tyypit ovat:

Tässä vaiheessa luomme enumeraatiollemme myös apumetodin, jonka avulla kullekin enumeraation arvolle saadaan vastaava väri peliruudukkoon. Kirjoita siis metodi getColor, joka ottaa parametrikseen GridType enumeraation arvon ja palauttaa Color-olion. Helpoin ja paras tapa toteuttaa tämä toiminnallisuus on käyttämällä match-rakennetta. Sen, millä väreillä kukin ruutu esitetään on täysin sinun päätettävissäsi.

Tässä vaiheessa olet edellisten lisäksi:

3. Osatehtävä - Pelilogiikka

Seuraavaksi rakennamme pelimme pohjaksi sen varsinaisen logiikkakoneiston, joka pitää yllä tilannekuvaa pelikentästä: seinien ja liikuteltavien esteiden paikoista sekä pelaajan ja maalin sijainneista. Laadimme pelilogiikan omaan luokkaansa, joka ei ole riippuvainen mistään tietystä käyttöliittymätoteutuksesta, vaan sitä voisi sopivan julkisen rajapinnan (eli sopivien metodien) kautta käyttää monella erilaisella graafisella ja ei-graafisella käyttöliittymällä. (Esimerkiksi periaatteessa voisit ottaa käyttöösi kaverisi luoman logiikkaluokan ja tämä toimisi sinun graafisen toteutuksesi kanssa.) Yleensä, ellei kyse ole hyvin yksinkertaisesta ohjelmasta, on hyvä ajatus erottaa sovelluslogiikka ja käyttöliittymä toisistaan — tällöin toiseen on helppo tehdä muutoksia ilman, että toista joudutaan merkittävästi sopeuttamaan muutoksiin.

Pelimme looginen ydin on luokka nimeltä Game. Se periytyy ainoastaan luokasta Object (eli se ei ole minkään Swing-luokan aliluokka). Luokka esittää pelikentän kaksiulotteisena taulukkona, jonka alkiot ovat juuri laaditun enumeraation GridType ilmentymiä.

Luokan Game konstruktori ottaa parametrinaan tiedoston (java.io.File), jonka pohjalta luodaan pelikenttä. Tiedoston tulee sisältää määrätyn muotoisia tekstirivejä, jotka luetaan java.util.Scanner-luokan avulla ja tulkitaan siten peliruudukon rakenteeksi. Esimerkki yksinkertaisesta pelikentästä on tiedostossa kentta1.txt

Lataa luokkasi pohjaksi seuraava tiedosto: Game.scala.

try-catch epäonnistuu
Muista liittää kaikki käsittelyä vaativat poikkeukset catch-lohkoon. Tässä puruluu on kaikille tuttu NullPointerException.

Huomaa, että luokan toteutuksen alussa on kummallinen try-catch -temppu. try-catch-finally on Scalassa käytetty rakenne, jonka avulla voidaan käsitellä poikkeuksia (eli luokan Exception ilmentymiä, joita pian sinäkin pääset käyttämään). try-lohko sisältää koodin, joka saattaa aiheuttaa poikkeuksen nämä mahdolliset poikkeukset sitten käsitellään catch-lohkossa.

catch-lohkossa voi määritellä yhden tai useammantyyppisen poikkeuksen käsittelyn. Huomaa kuitenkin, että jos ensimmäisenä ja/tai ainoana poikkeustyyppinä on Exception, kaikki poikkeukset jäävät tähän haaviin. catch-lohkon sisäinen rakenne perustuu case:jen määräämiin tapauksiin (samalla tavalla kuin match-rakenteessa). Edellisten lisäksi voidaan jatkona käyttää vielä finally-lohkoa, joka toteutetaan joka tapauksessa huolimatta siitä kuinka try-lohkon suoritus päättyy. Voit lukea tarkemman, esimerkein varustetun kuvauksen seuraavasta lähteestä: http://www.tutorialspoint.com/scala/scala_exception_handling.htm.

Kun toteutat omia try-catch-lohkoja, muista sijoittaa kaikki mahdollisesti virheen aiheuttavan käskyn lopputulosta tarvitsevasta koodista kokonaisuudessaan try-lohkon sisään, ettei käy niin kun hauvalle tuossa yllä. Tietenkään aina ei ole tarkoituksenmukaista olla koodimaailman Ash Ketchum ja napata kaikkia poikkeuksia. On tapauksia, jolloin ohjelman suorituksen tulisi loppua virheeseen.

Välikevennyksen voimin sitten varsinaiseen osatehtävän pihviin.

Toteuta ainakin seuraavat metodit luokkaan Game:

Kun Game-luokkasi on valmis, voit testata sen toiminnan seuraavan luokan avulla: SikobanTest.scala. Se käyttää ruudukkoa apuna yksinkertaisessa merkkipohjaisessa Sokoban-pelissä, jota voi pelata syöttämällä aina sen ruudun koordinaatit, johon pelaajan halutaan seuraavaksi siirtyvän.

Luomasi pelilogiikka on aika kytkeä osaksi edellä aloitettuun graafiseen käyttöliittymään.

Tässä vaiheessa olet edellisten lisäksi:

4. Osatehtävä - Luokka GamePanel

Seuraavaksi luodaan paneeli, joka kuvastaa ruudukoitua pelikenttää. GamePanel perii luokan GridPanel. Koska GridPanel-luokan konstruktori vaatii rivien ja sarakkeiden lukumäärät, täytyy vastaavanlainen konstruktori määrittää myös GamePanel-luokallemme. Edellisten parametrien lisäksi luokkamme konstruktori ottaa vielä äsken luomamme Game-luokan instanssin.

Tässä vaiheessa voit hetkeksi palata Sokoban luokan pariin ja liittää GamePanel:in osaksi käyttöliittymää. Sijoita tämä GamePanel aikaisemmin luomaasi BorderPanel-tyyppiseen säiliöön. Lisää myös Sokoban-luokkaasii vastaavalla tavalla kuin Game-luokassa try-catch-lohko, joka tässä kohti yrittää aloittaa uutta peliä annetulla kentta1.txt-tiedostolla. Tässä poikkeuksiin voidaan reagoida kaikkiin samalla tavalla, ainoastaan tulostamalla virheen viesti.

Käytämme GamePanel-luokassa ruudukon piirtämiseen OLO-tapauksessa esiteltyä Graphics2D-oliota. Ylikirjoitamme suojatun (protected, ei löydy Scala API:sta näkyvyytensä takia) metodin paintComponent, joka ottaa parametrinaan Graphics2D-olion. Metodin ei tule palauttaa mitään. paintComponent-metodia kutsutaan automaattisesti aina kun metodin edellisen kutsun suoritus on saatu päätökseen, mistä huolehtii Scalan Swing.

Meidän toteutuksessamme paintComponent-metodin toiminta etenee niin, että jokainen Game-luokan instanssin määräämä ruutu piirretään aikaisemmin luodun enumeraation GridType määrämällä värillä. Määritä piirtämiesi ruutujen kooksi (30, 30) pikseliä ja käytä Graphics2D-oliota vastaavasti kuin OLO-tapauksessa.

Seuraavaksi toteutamme pelikentän reaktiot käyttäjän toimintaan. Voit valita omien mieltymystesi mukaan haluatko pelisi toimivan seuraamalla käyttäjän hiiren liikkeitä vai klikkauksia (näppäimistön kuuntelu jätetään bonustehtäväksi). Lisää siis valitsemasi hiiren toiminnot luokan GamePanel kuunneltavaksi käyttämällä listenTo-metodia.

Määrittele sitten rektiot näihin klikkauksiin, muokkaamalla GamePanel:in reactions-kenttää. Saat selville hiiren aiheuttamasta tapahtumasta (joko MouseMoved tai MouseClicked) hiiren sen hetkisen kursorin sijainnen, minkä perusteella voit pyytää Game-luokan instanssilta siirtymistä siihen pelikentän ruutuun. Toteuta hiiren kuuntelu niin, että kun peli on saatu päätökseen pelaajan liikuttaminen ei enää onnistu.

Kokeile nyt myös graafisesti liikkuuko pelaaja oikein.

Tässä vaiheessa olet edellisten lisäksi:

5. Osatehtävä - Pelin status

Luodaan seuraavaksi mekanismi, joka antaa lisätietoa pelin tilanteesta. Luo Label-olio, johon pelin alussa asetetaan teksti, joka kuvastaa sitä, että peli on käynnissä. Muista liittää tämä osaksi Sokoban-pelisi käyttöliittymään peliruudukon alle.

Luo lisäksi julkinen metodi, joka ottaa parametrikseen merkkijonon ja asentaa sen Label:isi tekstiksi.

Nyt voit muokata aikaisemmin määrittelemäsi GamePanel-luokan reaktioihin tekstin muuttumisen pelin päätyttyä.

Tässä vaiheessa olet edellisten lisäksi:

6. Osatehtävä - Valikkojen kuuntelu

Nyt lisäämme ensimmäisessä osatehtävässäsi luomiin valikkoihin kuuntelijat.

Lopeta -valikko

Muokkaa valikkonimikkeen luontia niin, että se ottaa parametrinaan uuden Action-olion. Action-olio ottaa parametrinaan valikon nimen. Rakenna tälle instanssille luokkalohko, jossa toteutat apply-metodin ja asetat valikolle pikanäppäimen. Tässä pieni apu, josta voit lähteä liikkeelle:

contents += new MenuItem(new Action("valikon nimi") {
  def apply() { /* apply metodin sisältö */ }
  accelerator = Some( /* näppäinkomento, johon valikko reagoi */ )
})
Määrittelemme tässä siis uuden nimettömän luokan, jota käytämme vain tässä. Muokkaa nyt apply-metodisi lopettamaan ohjelmasi suorituksen. Pikanäppäimen määrittämiseen voit käyttää omaa harkintaasi, tyypillinen vaihtoehto olisi esimerkiksi alt+F4. Tarvitset tämän toteuttamiseen Java Swing-luokkakirjaston KeyStroke-luokkaa. Helpoin tapa on käyttää merkkijonon perametrikseen ottavaa luokan konstruktoria.

Aloita alusta -valikko

Pelin alusta aloittaminen toteutetaan samanlaisella rakenteella kuin edellä, mutta luonnollisesti erilaisella apply-metodin toiminnalla ja pikanäppäimellä. apply-metodisi tulisi päivittää ikkunasi alaosassa näkyvä teksti kuvastamaan sitä, että peli on aloitettu alusta ja huolehtia pelikentän saattamisesta alkuasetelmaan. Käytä tässä jälkimmäisessä tehtävässä pelilogiikka luokkaan Game luomaasi startOver-metodia.

Pikanäppäimen valinnassa voit käyttää jälleen omaa harkintakykyäsi, mutta tyypillinen vaihtoehto olisi F2-näppäin.

Tässä vaiheessa olet edellisten lisäksi:

7. Osatehtävä - Bonustehtäväehdotuksia

Tässä vaiheessa sinulla pitäisi olla toimiva Sokoban peli, joka toimii sellaisenaan.. mutta ei ehkä ole kovin monipuolinen eikä välttämättä näytä hyvältä. Voit kehittää peliäsi eteenpäin vielä muutamalla bonus-ominaisuudella (ei vaadita täysiin pisteisiin), joista muutamia esitellään seuraavassa osatehtävässä. Voit aina kehittää myös omia ominaisuuksiasi.

Päheämmät grafiikat

Lataamassasi luokat.zip-paketissa oli mukana myös kaksi kuvaa. Nämä kuvat on laitettu mukaan silmälläpitäen pelimme ulkoasua. Voit käyttää näitä kuvia tai luoda omasi saadaksesi peliin eloa. Voit käyttää kuvia esittämään pelaajaa (sika) ja estettä (betoniporsas)

Pystyt käyttämään kuvien piirtämiseen paintComponent-metodissa käyttämääsi samaista Graphics2D-oliota.

Kentän vaihtaminen

Lisää valikkoosi uusi nimike, joka on vastuussa pelikenttätiedoston vaihtamisesta. Toteuta valikkonimikkeelle kuuntelija vastaavasti kuin kuudennessa osatehtävässä. Sinun täytyy luoda tiedostonvalitsija ( FileChooser, joka hyväksyy tekstitiedostoja. Pystyt rajaamaan vain tiettyihin tiedostoihin FileFilter-luokan avulla.

Luo Game-luokkaasi uusi metodi, joka ylikirjoittaa startOver-metodin ottamalla parametrinaan File tyyppisen olion, jonka perusteella uusi peli aloitetaan.

Kenttien generointi (vaikea: 6p arvoinen)

Luo peliisi mekanismi, jolla generoidaan jokaisella pelikentällä (voitettavissa oleva) uusi sokkelo.

8. Osatehtävä - Lopuksi

Testaa vielä lopuksi kaikkien tekemiesi luokkien toiminta ja varmista, että kaikki toimii toivomallasi tavalla. Viimeistään tässä vaiheessa kommentoi koodisi ne pätkät, jotka saattavat olla vaikeaselkoisia. Pakkaa koko ratkaisusi niin, että tarkistavan assistentin ei tarvitse ladata mitään muuta lisäksi ohjelman toimimiseksi. Laita siis palautettavaan pakettiin ainakin seuraavat tiedostot: Game.scala, GamePanel.scala, GridType.scala, SikobanTest.scala, Sokoban.scala, kentta1.txt sekä mahdolliset kuvatiedostosi, jos olet luonut peliisi hienommat grafiikat. Tämän lisäksi lisää vielä pakettiisi readme, jonka pohjan voit kopioida alta.

# ME-C2120 Syksy 2013
#
# Kierros 5: Sokoban

Opiskelijanumero:
Vaadittuihin osiin käytetty aika tunteina (arvio):
Bonustehtäviin käytetty aika tunteina (arvio):

# Mitkä kierroksen osiot toteutit merkitse ne sanalla 'tehty'.
# Voit merkitä myös tekemäsi, mutta toteuttamatta jääneet kohdat sanalla 'yritetty'.
# Voit mainita, miksi näiden tehtävien toteuttaminen ei onnistunut.

O1 Kehys Sokoban: ei ole tehty
    Pelin ikkuna tehdään ja se näkyy oikeankokoisena:    ei ole tehty
    Pelin ikkuna avautuu keskelle näyttöä:               ei ole tehty
    Pelin ikkunalla on valikkorivi:                      ei ole tehty
O2 Enumeraatio GridType:                                 ei ole tehty
    Enumeraatio sisältää vaaditut tyypit:                ei ole tehty
    Enumeraatiolla on vaadittu apumetodi:                ei ole tehty
O3 Pelilogiikka:                                         ei ole tehty
    Pelikenttä tulkitaan oikein:                         ei ole tehty
    Pelaaja liikkuu sääntöjen mukaan pelissä:            ei ole tehty
    Peli voi päätyä:                                     ei ole tehty
O4 Luokka GamePanel:                                     ei ole tehty
    Pelissä tulkitaan kenttätiedosto graafisesti oikein: ei ole tehty 
    Pelisi reagoi käyttäjän komentoihin:                 ei ole tehty
O5 Pelin status:                                         ei ole tehty
    Pelin status päivittyy tarvittaessa:                 ei ole tehty
O6 Valikkojen kuuntelu:                                  ei ole tehty
    Pelin lopettava valikko toimii:                      ei ole tehty
    Pelin uudestaan aloittava valikko toimii:            ei ole tehty

# Teitkö annettuja bonustehtäviä tai jotain muita lisäominaisuuksia?
(Kerro mitä teit ja lisää mahdolliset käyttöohjeet, mikäli toiminnallisuus
poikkeaa suuresti vaaditusta kokoelmasta.)

# Jäikö ohjelmaasi virheitä tai kohtia, jotka eivät toimi oikein?
(Listaa kaikki tuntemasi ongelmasi. Mainitse, jos mahdollista, omat arviosi
näiden syyksi ja kuinka olet yrittänyt korjata ne.)

# Vapaat tehtävään liittyvät kommentit?
(Voit vapaasti kommentoida tehtävää, mutta muista ennen kaikkea jättää
varsinainen palaute palautelomakkeen kautta.)
Täytä readme.txt ja liitä sekin osaksi palautustasi. Nimeä palautettava pakettisi muotoon opnro_kierros5.zip (ei vitsiniekkoja tällä kertaa, vaan opnro:n tilalle oma opiskelijanumero :).

Palautus

Tehtävän palautus tehdään aikarajaan lauantaihin 23.11. klo 18.00 mennessä Rubyric-järjestelmään. Myöhästyneestä palautuksesta seuraa 2 pisteen vähennys alkavaa vuorokautta kohden – eli esimerkiksi sunnuntaina klo 18.01 palautetun työn pisteistä vähennetään 4 pistettä. Väärästä pakkausmuodosta (sallittuja .jar ja .zip) seuraa yhden pisteen sakko. .scala-tiedoston puuttumisesta seuraa yhden pisteen sakko. readme.txt-tiedoston puuttumisesta seuraa yhden pisteen sakko.

Palautuksen jälkeen käy vielä vastaamassa palautteeseen osoitteessa: http://www.cs.hut.fi/cgi-bin/teekysely.pl?action=showform&id=studio1-scala5-2013&lang=FIN

Arvostelu

Ohjelmointitehtävät arvostellaan erillisten kriteerien mukaisesti. Tehtävän maksimipistemäärä on 60 pistettä ja hyväksytty suoritus on vähintään 30 pistettä.

Jos tehtäväpalautus on selvästi hyvin keskeneräinen ja se saa alle vaaditun hyväksytyn pistemäärän, se voidaan palauttaa tekijälle korjattavaksi (ns. bumerangi). Tehtävä on hyväksytty vasta, kun se on palautettu riittävän laajuisena ja syvällisenä. Bumerangina palautetusta tehtävästä voidaan antaa korjattunakin enintään minimipistemäärä 30.

Kannattaa kiinnittää huomiota koodin ja sisennyksen selkeyteen; jos assistentti näkee koodista selkeästi ajatuksenkulkusi, helpottaa se koodin toimivuuden ja vikatilanteiden vakavuuden arviointia. Kannattaa myös varmistaa, että palautettava koodi kääntyy ja että kyseessä on viimeisin versio harjoituksestasi.

Lisäksi ohjelmointitehtävissä on mahdollista saada lisäpisteitä tehtävänannon ylittävästä suorituksesta