Tehtävät voivat vielä muuttua!
Nämä harjoitukset liittyvät oppimateriaalin lukuihin 8 ja 9.
Kaikki harjoitustehtävät on syytä tehdä. Jotkin tehtävät on merkitty keltaisella värillä. Ne ovat ehkä hieman muita haastavampia. Ilman niitäkin harjoituksista voi saada maksimipisteet, mutta ne lasketaan silti mukaan harjoituspisteitä määrättäessä – ne voivat siis korvata joitakin haasteettomampia tehtäviä tms. Mutta ennen kaikkea noista keltaisista tehtävistä sitä vasta oppiikin!
Huom:
// 4. harjoitukset, tehtävä 2.2, Oili Opiskelija
Tämän sarjan tehtävät palautetaan sähköpostitse "pajasuurmestari" Arto Vihavaiselle (arto.vihavainen (at) helsinki.fi). Koko sarja lähtetään yhtenä sähköpostina. Lähetä siis vasta, kun olet tehnyt kaikki kohdat, jotka aioit tehdä! Posti on otsikoitava täsmälleen muodossa "OHJA V4, TEHTÄVÄ 1" (ilman sitaatteja). Automaattinen filtteri ohjaa postit oikeaan paikkaansa. Virheellisesti otsikoituja viestejä ei oteta lainkaan huomioon.
Pisteet saa kunnollisista selityksistä: lepsuilusta tai ympäripyöreistä käsienheilutteluista pisteitä ei saa. Anna jokaisesta kohdasta pieni itse laadittu ohjelmaesimerkki!
Huomaa että tehtävä merkitään tehdyksi vain, jos tehtävän jokaisen kohdan selitys on oikein ja ymmärrettävissä – esimerkiksi tehtävässä 1.1. tulee osata selittää kaikki termit kohdasta yksi kohtaan kolme. Valitettavasti resurssit eivät riitä henkilökohtaiseen palautteeseen, mutta asoiden pitäisi selvitä kurssimateriaalista.
Älä tee tätä tehtäväsarjaa pajassa, jos siellä on ruuhkaa!
Jos et pääse alkuun, ajattele selittäväsi käsitteitä kurssikaverillesi, joka on ohjelmointitaidoiltaan hieman sinua jäljessä. Tähän tehtävään ei saa henkilökohtaista ohjausta pajassa eikä henkilökohtaista palautetta sähköpostitse, mutta kuten todettu asioiden pitäisi selvitä kurssimateriaalista. Ja kurssimateriaalin esitystaso ja tyyli myös riittää vastaukseksi!
["1.0" Kertausta: ]
Harjoitellaan luennolla nähdyn pienen "sovelluskehyksen" kehittelyä ja käyttöä. Tarkoitus on kehitellä luokkaa, josta voidaan erikoistaa yksinkertaisia pelejä, jotka perustuvat kahden pelaajan String-muotoisiin vastauksiin. Oletuksena on myös, että kaikissa pelitilanteissa on voittaja. Tasapelejä kehys ei tunne.
Sovelluskehys tarjoaa pelin toteuttajalle erilaisia palveluita, kunhan sovelluksen luoja vain ohjelmoi itse päätössäännön, jolla yhden pelin (pelikierroksen) voittaja ratkaistaan.
Teknisesti päätössäännön vaatiminen on toteutettu pelikehyksen abstraktina metodina:
public abstract class Pelikehys { // Tähän koukkuun ripustetaan todellinen päätössääntö, jolla voittajan valitaan: public abstract boolean ekaVoittaa(String eka, String toka); // Kehyksen tarjoamat valmiit palvelut: public void tulostaTulos(String eka, String toka) { if (ekaVoittaa(eka, toka)) System.out.println("Ensimmäinen voittaa!"); else System.out.println("Toinen voittaa!"); } }
Esimerkkinä kahden pelaajan peli, jossa pidemmän merkkijonon antaja voittaa. Jos merkkijonot ovat yhtä pitkät, toinen voittaa. Sovellus ohjelmoidaan abstraktin Pelikehys-luokan aliluokkana. Voittajan valitsevalle päätössäännölle, metodille ekaVoittaa annetaan toteutus:
import java.util.Scanner; public class PidempiVoittaa extends Pelikehys { private static Scanner lukija = new Scanner(System.in); public boolean ekaVoittaa(String eka, String toka) { return eka.length() > toka.length(); } public static void main(String[] args) { PidempiVoittaa peli = new PidempiVoittaa(); String eka, toka; System.out.print("1. pelaajan vastaus: "); eka = lukija.nextLine(); System.out.print("2. pelaajan vastaus: "); toka = lukija.nextLine(); peli.tulostaTulos(eka, toka); } }
Huomaa miten pääohjelmassa luodaan oman luokan ilmentymä, jolla sitten pelataan.
Toinen esimerkki, puhdas onnenpeli:
import java.util.Scanner; public class Onnetar extends Pelikehys { private static Scanner lukija = new Scanner(System.in); public boolean ekaVoittaa(String eka, String toka) { return Math.random() < 0.5; // arvotaan voittaja, // molemmat yhtä todennäköisiä } public static void main(String[] args) { Onnetar peli = new Onnetar(); System.out.print("1. pelaajan vastaus: "); lukija.nextLine(); // vastauksella ei väliä! System.out.print("2. pelaajan vastaus: "); lukija.nextLine(); peli.tulostaTulos("", ""); // vastauksilla ei väliä! } }
Pelikehys tarjoaa pelituloksen selvittämiseen vain palvelun, joka itse kirjoittaa tuloksen. Tämä ei ole aina toivottavaa: Sovellus voi haluta keskustella erilaisin sanakääntein tai vaikkapa toisella kielellä. Ehkä jopa halutaan jättää yksittäinen pelitulos raportoimatta, jos vaikkapa pelissä on useita kierroksia.
Täydennä kehystä metodilla joka vaiteliaana palauttaa tiedon voittajasta:
public boolean tulos(String eka, String toka)
Metodi palauttaa true, jos ensimmäinen voittaa ja false jos toinen voittaa. Tämä metodi siis toimii vielä täsmälleen samoin kuin metodi ekaVoittaa, mutta odotapa vain, kun päästään kehittelemään kehystä monipuolisemmaksi – esimerkiksi tilastoinnin taidossa...
Huomaa miten tämä ei-abstrakti metodi ekaVoittaa-metodin tapaan kutsuu abstraktia "koukkua".
Havainnollista uuden metodin käyttöa muokkaamalla PidempiVoittaa-sovelluksesta versio, joka keskustelee muuten kuin alkuperäinen, mutta englanniksi. Ohjelman viestitekstit siis ovat "First wins!", "Second wins!", "1. answer: " ja "2. answer: ".
Tässä tehtävässä siis sovellus käyttää tulostaTulos-metodin sijaan metodia tulos.
Muokkaa Onnetar-sovelluksesta sovellus KoneVaiMina, jossa käyttäjä ja tietokone pelaavat onnenpeliä seuraavaan tapaan:
Kumpi voittaa, sinä vai minä? Paina enter! Minä voitin!
Kone siis kirjoittaa kaiken, käyttäjä vain painaa enter-näppäintä. Tarvinnet sovelluksessasi uudempaa vaiteliasta tulos-metodia.
Pelikehys-luokan aliluokkana toteutettu sovellus voi tietenkin kutsua tulostaTulos- ja/tai tulos-metodia useampaankin kertaan, jos pelissä on jossakin mielessä useampia kierroksia. (Tässä "kierros" tarkoittaa yhtä kertaa selvittää voittaja.)
PidempiVoittaa peli = new PidempiVoittaa(); ... while (...) { ... peli.tulostaTulos(eka, toka); ... } ...
Tällaisessa tilanteessa saattaa olla tarpeen tietää tilastoja voitoista, tappioista ja kierrosten määrästä.
Lisää Pelikehys-luokkaan tilastointipalveluja:
Kierroksia: 26 1-voittoja: 19 2-voittoja: 7
Tätä metodia voi käyttää, ellei ole tarpeen itse muotoilla tilastojen tulostusasua.
Vihjeitä:
Muokkaa Onnetar-sovelluksesta sovellus KoneVaiMinaSarja, jolla käyttäjä ja tietokone pelaavat kokonaisen sarjan eriä:
Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! Minä voitin! Tilanne 0-1 Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! Sinä voitit! Tilanne 1-1 Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! Sinä voitit! Tilanne 2-1 Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! Minä voitin! Tilanne 2-2 Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! Sinä voitit! Tilanne 3-1 Kumpi voittaa, sinä vai minä? Enter jatkaa, muu lopettaa! sadaöd Kierroksia: 5 1-voittoja: 3 2-voittoja: 2
Kone siis kirjoittaa kaiken nähdyn, käyttäjä vain painelee enter-näppäintä, paitsi lopetukseksi käyttäjä kirjoittaa jotakin, mitä vain, ennen enter-näppäimen painallusta.
Peli perustuu valittujen sanojen alkukirjaimeen: Ensimmäinen pelaaja voittaa, jos molemmat valitsevat vokaalilla alkavan sanan tai jos molemmat valitsevat konsonantilla alkavan sanan. Jos valittujen sanojen alkukirjaimista toinen on vokaali ja toinen konsonantti, toinen pelaaja voittaa.
Toteuta pelin kahden pelaajan versio Pelikehys-luokan aliluokkana.
Vihje: Yksi helppo tapa tutkia, onko merkkijonon ensimmäinen merkki skandinaavisen aakkoston vokaali:
if ("AEIOUYÅÄÖaeiouyäåö".indexOf(jono.charAt(0)) != -1) ) ...
Vihje: On tyylikästä tehdä vokaalitarkistuksesta erillinen apumetodi!
Kannattaa ehkä ensin tehdä alustava versio, jossa pelataan vain yksi kierros. Kun se toimii, laajenna peli monikierroksiseksi.
Lopullisen version käyttöesimerkki (ihmisen tekstit esimerkissä kursiivilla):
1. pelaajan sana? (Pelkkä enter lopettaa.) aamu 2. pelaajan sana? (Pelkkä enter lopettaa.) ilta 1. pelaajan sana? (Pelkkä enter lopettaa.) talvi 2. pelaajan sana? (Pelkkä enter lopettaa.) uppotukki 1. pelaajan sana? (Pelkkä enter lopettaa.) Kierroksia: 2 1-voittoja: 1 2-voittoja: 1
Jos pelkkä enter annetaan toisen pelaajaan vastauksena, tätä viimeista kierrosta ei lasketa mukaan.
Toteuta edellisen kaltainen peli, jossa ihminen pelaa konetta vastaan tyyliin (ihmisen tekstit esimerkissä kursiivilla):
Olen arvannut sanan. Mikä on arvauksesi: kissa Minä voitin koska arvaukseni oli "qwerty" Olen arvannut sanan. Mikä on arvauksesi: aamiainen Sinä voitit, koska arvaukseni oli "sdfg" Olen arvannut sanan. Mikä on arvauksesi: pekonihampurilainen Minä voitin koska arvaukseni oli "kissa" Olen arvannut sanan. Mikä on arvauksesi: Kierroksia: 3 1-voittoja: 2 2-voittoja: 1
Pelaajan tyhjä arvaus lopettaa pelin. Kone siis on ensimmäinen pelaaja, ihminen toinen.
Koneen sananvalinnan logiikkaa ei ole annettu. Kehittele se itse. Voit halutessasi ja osatessasi kehitellä ohjelmalle tekoälyä esimerkiksi tallettamalla pelaajan arvauksia ArrayList-rakenteeseen ja analysoimalla historiaa.
Edellä tarkasteltiin äärimmilleen yksinkertaistettua "sovelluskehystä". Sormiharjoittelua! On aika alkaa harjoitella vähän isompiin esimerkkeihin perehtymistä. Perehdy alla olevaan Luukkaisen Matin hienompaan pelikehykseen KaksinpeliKehys.
Huomaa yksi uusi tärkeä tekniikka: sovelluskehyksessä voidaan toteuttaa myös ei-abstrakteja oletustoteutuksia metodeille. Toteutus voi olla myös tyhjä algoritmi: ei tehdä mitään. Jos sovelluksessa halutaan poiketa oletuksesta, peritty oletus syrjäytetään eli ohjelmoidaan perityn metodin syrjäyttävä eli korvaava (overriding) metodi!
// Matti Luukkaisen pelikehysesimerkki: import java.util.Scanner; public abstract class KaksinpeliKehys { private int ykkosenVoitot; private int kakkosenVoitot; private int tasapelit; private int kierrokset; private Scanner lukija = new Scanner(System.in); /** * Metodia kutsuttaessa pelataan yksi kierros. Metodi * palauttaa true jos kumpikaan pelaaja ei halunnut lopettaa. * * Kierroksen toiminnallisuus määritellään aliluokassa syrjäyttämällä * pelaaKierros-metodin kutsumat abstraktit ja tyhjät metodit. */ public boolean pelaaKierros(){ tulostaOhje(); String eka = ekanVastaus(); if ( lopetuksenTarkastus(eka) ) return false; String toka = tokanVastaus(); if ( lopetuksenTarkastus(toka) ) return false; int voittaja = selvitaVoittaja(eka, toka); if ( voittaja==1 ) ykkosenVoitot++; else if ( voittaja==2 ) kakkosenVoitot++; else tasapelit++; kierrokset++; tulostaKierroksenTiedot( voittaja ); if ( jokaKierroksellaStatistiikka() ) tulostaStatistiikka(); return true; } /* * Koukkumetodi, joka on pakko syrjäyttää: * * palauttaa 1 jos voittaja on pelaaja 1 * 2 jos voittaja on pelaaja 2 * jonkun muun luvun tasapelin tapauksessa */ public abstract int selvitaVoittaja(String eka, String toka); // Oletustoiminnallisuudet, jotka SAA syrjäyttää aliluokassa. // tulostaa joka kierroksella ohjeen pelaajille public void tulostaOhje() { } // palauttaa ensimmäisen pelaajan vastauksen public String ekanVastaus(){ System.out.print( pelaaja1() +":n vastaus: "); return lukija.nextLine(); } // palauttaa toisen pelaajan vastauksen public String tokanVastaus() { System.out.print( pelaaja2() +":n vastaus: "); return lukija.nextLine(); } // palauttaa true jos pelaajan syöte on ilmaisee pelin lopetuksen public boolean lopetuksenTarkastus(String syote) { return syote.equals(""); } // palauttaa true jos joka kierroksella halutaan tulostaa tilastotietoja public boolean jokaKierroksellaStatistiikka() { return true; } // tulostaa pelikierroksen tuloksen public void tulostaKierroksenTiedot(int voittaja) { if ( voittaja==1 ) System.out.println("voittaja "+ pelaaja1()); else if ( voittaja==2 ) System.out.println("voittaja "+ pelaaja2()); else System.out.println("tasapeli"); } // tulostaa tilastotiedot public void tulostaStatistiikka() { System.out.println( "kierroksia " +kierrokset ); System.out.println( pelaaja1()+" - "+pelaaja2()+ " " + ykkosenVoitot+ "-"+kakkosenVoitot ); } // pelaajien nimet voidaan vaihtaa syrjäyttämällä seuraavat public String pelaaja1(){ return "pelaaja 1"; } public String pelaaja2(){ return "pelaaja 2"; } }
Käyttöesimerkki:
public class PidempiVoittaa extends KaksinPelikehys { // Syrjäyttävä metodi toteuttaa voittajan valinnan: public int selvitaVoittaja(String eka, String toka) { if (eka.length() > toka.length()) { return 1; } else if (eka.length() < toka.length()) { return 2; } return 0; } public static void main(String[] args) { PidempiVoittaa peli = new PidempiVoittaa(); System.out.println("Peli alkaa, jos toinen pelaaja antaa tyhjän syötteen, lopetetaan"); // jatketaan ikuisesti while (true) { // paitsi jos pelaaKierros palauttaa false if (!peli.pelaaKierros()) break; } } }
Jo Ohjelmoinnin perusteet -kurssilla tutuksi tullutta kivi-paperi-sakset-peliä pelataan siten, että pelaajat valitsevat samanaikaisesti yhden vaihtoehdoista kivi, paperi ja sakset. Kivi voittaa sakset, sakset voittavat paperin ja paperi voittaa kiven.
Toteuta kahden pelaajan kivi-paperi-sakset-peli KaksinpeliKehys-luokan aliluokkana. Virheelliset syötteet on syytä tarkistaa ja karsia.
Ohjelma kysyy aluksi pelaajien nimet, jotta se osaa ilmoittaa tuloksen kohteliaasti voittajan nimenkin mainiten.
Tee ensin versio, jossa pelataan vain yksi kierros. Kun se toimii, laajenna peli monikierroksiseksi.
Toteuta kivi-paperi-sakset-tietokonepeli KaksinpeliKehys-luokan aliluokkana. Ohjelmoi koneelle myös mahdollisimman hyvä (?) tekoäly. Miten se tehdään, onkin sitten mietinnän paikka!
Yksi mahdollinen (vaikkei välttämättä paras?) tapa voisi olla pitää kirjaa tehdyistä siirroista vaikkapa ArrayList-oliossa ja pyrkiä historiatiedoista päättelemään, millä tavoin peliä pelaava ihminen toimii.
Toinen mahdollisuus on vaikkapa vain tilastoida, miten usein pelaaja on valinnut kunkin vaihtoehdon ja painottaa koneen valinnat tilastollisiksi voittajavalinnoiksi.
Ja jos oikein hurjaksi heittäydyt, voit tutkia miten hyvin erilaiset tekoälyviritelmäsi pärjäävät versiolle, joka täysin satunnaisesti valitsee koneen vaihtoehdon. Ennen hurjaksi heittäytymistäsi muista kuitenkin tehdä muut tehtävät ensin...;-)
Muuton yhteydessa tarvitaan muuttolaatikoita. Laatikoihin talletetaan erilaisia esineitä. Kaikkien laatikoihin talletettavien esineiden on toteutettava seuraava rajapintaluokka:
public interface Talletettava { public double getPaino(); }
NetBeans-käyttäjä: Lisää rajapintaluokka ohjelmaasi. Rajapinta lisätään melkein samalla tavalla kuin luokka, new Java class sijaan valitaan new Java interface.
Komentotulkin käyttäjä: Sijoita rajapintaluokka samaan hakemistoon tässä tehtävässä ohjelmoitavien luokkien kanssa.
Tee rajapintaluokan toteuttavat luokat Kirja
ja CDLevy
.
Kirjan paino vaihtelee ja se annetaan konstruktorin parametrina.
Kaikkien CD-levyjen paino sen sijaan on 0.1 kg.
Koska luokat toteuttavat rajapinnan, ne täytyy määritellä seuraavasti:
public class Kirja implements Talletettava { //... }
public class CDLevy implements Talletettava { // ... }
Luokkia voi käyttää vaikkapa seuraavan tapaan:
public static void main(String[] args) { Kirja kirja1 = new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2); Kirja kirja2 = new Kirja("Robert Martin", "Clean Code", 1); Kirja kirja3 = new Kirja("Kent Beck", "Test Driven Development", 0.5); CDLevy cd1 = new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); CDLevy cd2 = new CDLevy("Wigwam", "Nuclear Nightclub", 1975); System.out.println(kirja1); System.out.println(kirja2); System.out.println(kirja3); System.out.println(cd1); System.out.println(cd2); }
Tulostus:
Fedor Dostojevski: Rikos ja Rangaistus Robert Martin: Clean Code Kent Beck: Test Driven Development Pink Floyd: Dark Side of the Moon (1973) Wigwam: Nuclear Nightclub (1975)
Muotoile tulostus juuri yllä nähdyllä tavalla, niin automaatti saadaan opetettua tarkastamaan neljännen tehtäväsarjan tehtävät. Tulostuksessa painoa ei siis ilmoiteta.
Tämä perustelu on tosin aika huono ja opiskelijan luovuutta rajoittava! Voit ihan omana harjoitteluna muokata ohjelmasta sellaisen, jossa kirjoilla ja levyillä voi myös olla muita ominaisuuksia kuin esimerkissä.
Olisi kätevää, jos voisimme sijoittaa vaikkapa
ArrayList-olioon millaisia tahansa esineitä, joiden
luokka toteuttaa Talletettava
-rajapintaluokan.
Ja sehän onnistuu:
ArrayList<Talletettava> tavarat = new ArrayList<Talletettava>(); tavarat.add( new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2) ); // ...
Kun listaan talletettavien alkioiden tyypiksi on määritelty
Talletettava
, listalle voi laittaa
minkä tahansa olion, jonka luokka
toteuttaa rajapinnan Talletettava
!
Muokka edellisen tehtävän ohjelma sellaiseksi, että tavarat viedään ensin listaan ja sitten tulostetaan listan alkiot.
Ohjelmoi luokka Laatikko, jonka ilmentymiin
voidaan tallettaa Talletettava
-rajapintaluokan toteuttavia
tavaroita. Konstruktorille annetaan maksimipaino, jota enempää
Laatikko-olio ei pysty säilömään.
Jos lisättävä ei mahdu laatikkoon, void-metodi lisaa vain
vaiteliaana jättää lisäämättä.
Esimerkki laatikon käytöstä:
public static void main(String[] args) { Laatikko laatikko = new Laatikko(10); laatikko.lisaa( new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2) ) ; laatikko.lisaa( new Kirja("Robert Martin", "Clean Code", 1) ); laatikko.lisaa( new Kirja("Kent Beck", "Test Driven Development", 0.5) ); laatikko.lisaa( new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973) ); laatikko.lisaa( new CDLevy("Wigwam", "Nuclear Nightclub", 1975) ); System.out.println( laatikko ); }
Tulostus:
Laatikko: 5 esinettä, paino yhteensä 3.7 kiloa
Jos teit laatikon sisälle ilmentymämuuttujan
private double paino
,
joka muistaa laatikossa olevien esineiden painon,
korvaa se metodilla, joka laskee painon:
public class Laatikko { //... public double getPaino() { double paino = 0; // laske laatikkoon talletettujen yhteispaino return paino; } }
Kun paino on tutkittava, esim. uuden tavaran lisäyksen yhteydessä, riittää siis kutsua laatikon painon laskevaa metodia.
Metodi toki voisi palauttaa myös ilmentymämuuttujan arvon. Harjoittelemme tässä kuitenkin tilannetta, jossa ilmentymämuuttujaa ei tarvitse eksplisiittisesti ylläpitää vaan se voidaan tarpeen tullen laskea. Seuraavan kohdan jälkeen laatikkoon talletettu painotieto ei kuitenkaan välttämättä enää toimisi. Miksi?
Alussa määritelty rajapintaluokka siis on seuraavanlainen
public interface Talletettava { double getPaino(); }
Rajapintaluokan toteutus siis vaatii, että luokassa on metodi
double getPaino()
.
Ja Laatikko-luokassa itsessäänkin on juuri tämä
metodi! Laatikoistakin voidaan siis tehdä laatikkoon talletettavia:
public class Laatikko implements Talletettava { // ... }
Laatikot ovat oliota joihin voidaan laittaa
Talletettava
-rajapinnan toteuttavia olioita.
Nyt kun laatikot itsekin ovat talletettavia,
laatikon sisällä voi olla myös laatikoita!
Kokeile että näin varmasti on: Luo ohjelmassasi muutama laatikko, laita laatikoihin tavaroita ja laita pienempiä laatikoita isompien laatikoiden sisään.
Kokeile, mitä tapahtuu, jos laitat laatikon itsensä sisälle.