Materiaalin copyright © Arto Wikla. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

Ohjelmoinnin jatkokurssi: harjoitukset s2010: 4/6 (22.11.-26.11.)

(Muutettu viimeksi 22.11.2010 klo 13:12, sivu perustettu 16.11.2010.)

Harjoitustehtävien otsikoilla on värikoodaus: Vihreät tehtävät on syytä tehdä joka tapauksessa. Värittämättömiä ei ole ihan pakko tehdä, mutta nekin ovat hyvin hyödyllisiä ja myös vaikuttavat pisteisiin. Keltaiset tehtävät ovat vähän haastavampia. Nekin lasketaan mukaan harjoituspisteitä määrättäessä, mutta ilmankin niitä harjoituksista voi saada maksimipisteet.

Huom: Jokaisen ohjelmatiedoston alkuun on kirjoitettava kommenttina harjoituskerta, tehtävän numero ja tekijän nimi tyyliin:

// 4. harjoitukset, tehtävä 3.1, Oili Opiskelija

Huom: Ohjaajien eli pajamestarien sivulta löytyy hienoja testauksen apuvälineitä tehtävien tekemisen avuksi!

Tehtävät saattavat muuttua vieläkin!

Pelisovelluskehys – metodeja koukussa

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, parempaa palvelua 1

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)

Tämä metodin versio siis ei tulosta mitään. Metodi saattaa aluksi tuntua turhalta, miksi vain kutsua ekaVoittaa-metodia, mutta odotapa vain kun päästään kehittelemään kehystä monipuolisemmaksi – esimerkiksi tilastoinnin taidossa...

Havainnollista uuden metodin käyttöa muokkaamalla PidempiVoittaa-sovelluksesta versio, joka itse hoitaa kaiken keskustelun.

KoneVaiMina.java, vaihe 1

Muokkaa Onnetar-sovelluksesta sellainen, 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, parempaa palvelua 2

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:

Tarvinnet myös joitakin uusia private-kenttiä sovelluskehykseen.

KoneVaiMina.java, vaihe 2

Muokkaa Onnetar-sovellusta siten, että 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.

VokaaliVaiKonsonantti.java

Peli perustuu valittujen sanojen alkukirjaimeen: Jos molemmat pelaajat valitsevat vokaalilla alkavan sanan, ensimmäinen pelaaja voittaa. Jos molemmat pelaajat valitsevat konsonantilla alkavan sanan, ensimmäinen pelaaja voittaa. Jos pelaajista jompikumpi valitsee vokaalilla alkavan sanan ja toinen konsonantilla alkavan sanan, toinen pelaaja voittaa.

Toteuta pelin kahden pelaajan versio Pelikehys-luokan aliluokkana.

Vihje: Yksi helppo tapa tutkia, onko merkkijonon ensimmäinen merkki vokaali:

if ("AEIOUYÅÄÖaeiouyäåö".indexOf(jono.charAt(0)) != -1) ) ...

Vihje: On varmaankin tyylikästä tehdä vokaalitarkistuksesta erillinen apumetodi?

Voit tehdä ensin version, jossa pelataan vain yksi kierros. Halutessasi voit laajentaa pelin monikierroksiseksi.

VokaaliVaiKonsonanttiKone.java

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.

Hienompi pelikehys

Edellä tarkasteltiin äärimmilleen yksinkertaistettua "sovelluskehystä". Sormiharjoittelua! On aika alkaa harjoitella vähän isompiin esimerkkeihin perehtymistä. Perehdy alla olevaan Luukkaisen Matin pelikehykseen (jolle ilman lupaa annoin nimen PelikehysPlus...)

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!

// Matti Luukkaisen pelikehysesimerkki:

import java.util.Scanner;
public abstract class PelikehysPlus {

    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 PitempiVoittaa extends Pelikehys {

    // 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) {
        PitempiVoittaa peli = new PitempiVoittaa();
        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;
        }
    }
} 

Kivi-paperi-sakset

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 PelikehysPlus-luokan aliluokkana. Virheelliset syötteet on syytä tarkistaa ja karsia. Voit tehdä ensin version, jossa pelataan vain yksi kierros. Halutessasi voit laajentaa pelin monikierroksiseksi.

Kivi-paperi-sakset-turnaus

Toteuta kivi-paperi-sakset-tietokonepeli PelikehysPlus-luokan aliluokkana. Ohjelmoi koneelle myös mahdollisimman hyvä (?) tekoäly.

Valmista järjestystä

Opiskelijat pistejärjestyksessä

Tämä tehtävä on muokattu kevään 2010 kurssiversion materiaalista. © Matti Luukkainen, Matti Paksula, Arto Vihavainen, Arto Wikla.

Ensimmäisen viikon tehtävässä 1.2 ohjelmoitiin Opiskelija-luokkaan metodi

public int compareTo(Opiskelija verrattava)

Tämä metodi – kuten ehkä joku vielä muistaa – "vertaa this-opiskelijaa parametriopiskelijaan. Metodi palauttaa arvon -1, jos this-opiskelija edeltää verrattava-opiskelijaa, arvon +1 päinvastaisessa tapauksessa. Jos opiskelijat ovat kaikilta tiedoiltaan samat, metodi palauttaa arvon 0. String-olioiden vertailussa tyydytään siis siihen 'aakkosjärjestykseen', jonka String-luokan compareTo-metodi määrittelee".

Kerro kääntäjällekin, että Opiskelija-olioille on totetutettu tällainen vertailtavuus. Kertominen tapahtuu ilmoittamalla luokan otsikossa, että luokka toteuttaaa rajapintaluokan Comparable<Opiskelija>.

Tämän jälkeen kääntäjä sallii Opiskelija-olioita käsiteltävän yleiskäyttöisillä työkaluilla, jotka on ohjelmoitu osaamaan käyttää kaikkia olioita, joiden luokka toteuttaa rajapintaluokan Comparable. (Vrt: Väline osaa lypsää kaikkia eläimiä, joiden luokka totetuttaa rajapintaluokan Lypsava.)

Tutkaile Java-APIn Collections-luokan tarjoamia palveluja, jotka ovat käytettävissä olioille, joiden luokka toteuttaa Comparablen ja hämmästy lLuokka on pakkauksessa java.util). Luokassa on paljon tavaraa, jota opituilla tiedoilla ei voi vielä ymmärtää, mutta kurkista nyt näin alkuun muutamaa kivan näköistä kirjastoitua luokkametodia: binarySearch, max, min, sort.

Kokeile seuraavaa:

ArrayList<Opiskelija> opiskelijat = new ArrayList<Opiskelija>();
Opiskelija mattiP = new Opiskelija("Matti", "P");
mattiP.setKoepisteet(25);
mattiP.setHarjoituspisteet(25);
opiskelijat.add(mattiP);
Opiskelija mattiV = new Opiskelija("Matti", "V");
mattiV.setKoepisteet(10);
mattiV.setHarjoituspisteet(29);
opiskelijat.add(mattiV);
Opiskelija mattiL = new Opiskelija("Matti", "L");
mattiL.setKoepisteet(16);
mattiL.setHarjoituspisteet(29);
opiskelijat.add(mattiL);

System.out.println("Opiskelijat alkuperäisessä järjestyksessä:");
for(Opiskelija opiskelija: opiskelijat) {
  System.out.println(opiskelija.getSukunimi() + ", " + opiskelija.getEtunimi());
}
      
Collections.sort(opiskelijat);

System.out.println();
System.out.println("Opiskelijat järjestettynä:");
for(int i = 0; i < opiskelijat.size(); i++) {
  Opiskelija op = opiskelijat.get(i);
  System.out.println((i+1) + ": " + op.getSukunimi() + ", " + op.getEtunimi());
}

Tulostuksen tulee olla seuraavanlainen:

Opiskelijat alkuperäisessä järjestyksessä:
P, Matti
V, Matti
L, Matti

Opiskelijat järjestettynä:
1: L, Matti
2: P, Matti
3: V, Matti

Tässä tehtävässä on paljon luettavaa (ja toivottavasti ymmärrettävää!) ja varsin vähän ohjelmointia...

MyStringistä Comparable ja heti tuotantoon

Ensimmäisen viikon tehtävässä 2.4 myös MyString-olioille opetettiin järjestys. Tee ominaisuudesta tunnettu samaan tapaan kuin edellisessä tehtävässä julkistettiin Opiskelija-olioiden järjestyvyys.

Nyt voit melko vaivatta ohjelmoida MyString-toteutuksen peruskurssin perinteisen koetehtävän kaltaiselle palvelulle:

Toteuta seuraava arvauspeli vuorovaikutteisena eli keskustelevana ohjelmana: Aamuisin ohjelmalle syötetään jokin määrä merkkijonoja missä järjestyksessä tahansa; olkoot nämä merkkijonot onnensanoja. Sama onnensana saa esiintyä useamminkin kuin kerran. Onnensanojen syötön päättää sana "loppu", joka ei itse ole onnesana.

Päivän mittaan pelaajat käyvät sitten arvaamassa sanoja. Jos pelaaja onnistuu arvaamaan jonkin onnensanan, ohjelma onnittelee pelaajaa. Jos pelaaja epäonnistuu, ohjelma esittää valittelunsa. Ohjelman suoritus päättyy, kun arvatuksi sanaksi syötetään "loppu". Tällöin ohjelma tulostaa oikeiden ja väärien arvausten määrän. Tehokkuussyistä sanojen hakeminen taulukosta on ohjelmoitava binäärihakua käyttäen.

Vihje: Collections varmaan helpottaa ohjelmoijan elämää tässäkin tehtävässä?

Joukot järjestykseen

Kokonaislukujoukkojen järjestys määräytyköön joukkojen mahtavuuden eli alkiolukumäärän perusteella. Muokkaa jostakin Kokonaislukujoukko-luokan toteutuksestasi järjestyvä ja havainnollista ominaisuuden käyttöä pienellä sovelluksella.

Lisäharjoituksia:

Arto Vihavaisen mainio tehtäväsarja, joka havainnollistaa tämän viikon tekniikoiden tyylikästä käyttöä.