Tämä ei ole uusin versio kurssista. Katso myös Ohjelmoinnin MOOC 2019: https://ohjelmointi-19.mooc.fi.
Tehtävät
Seitsemännen osan tavoitteet

Osaa pilkkoa ohjelman useampaan osaan, missä yhden vastuulla on (tekstimuotoinen) käyttöliittymä, toinen vastuulla sovelluslogiikka. Tuntee testivetoisen ohjelmistokehityksen perusajatuksen ja osaa kirjoittaa ohjelman metodien toiminnallisuutta tarkastelevia testejä. Ryhmittelee arvoja hajautustaulun avulla ja osaa käyttää listaa hajautustaulun arvona. Osaa luoda yksinkertaisia simulaatioita.

Kurssin väli/loppukysely

Kurssiin kuuluu kyselyitä, joiden tavoitteena on parantaa kurssia. Vastaa kyselyyn osoitteessa https://elomake.helsinki.fi/lomakkeet/85847/lomake.html.

Käy vastaamassa ensin kyselyyn ja ruksaa sen jälkeen allaoleva tekstikenttä. Jos allaolevan tekstikentän ruksaaminen ei onnistu, varmista että olet kirjautunut tälle sivulle. Kirjautuminen onnistuu sivun oikeasta ylälaidasta.

Käyttöliittymän ja sovelluslogiikan eriyttäminen

Edellisessä osassa tarkasteltiin sovelluksen jakamista vastuualueisiin. Jatketaan tässä samalla teemalla.

Suurin syy käyttöliittymän ja sovelluslogiikan eriyttämiseen on ohjelman ylläpidettävyyden ja ymmärrettävyyden lisääminen. Sovellluslogiikan eri osa-alueet ovat ideaalitilanteessa käyttöliittymäriippumattomia, jolloin niitä voidaan parannella ilman käyttöliittymään koskemista. Vastaavasti osia voidaan siirtää sovelluksesta toiseen, jolloin käyttöliittymä on esimerkiksi vaihdettavissa tekstikäyttöliittymästä graafiseen käyttöliittymään.

Edellä kuvattu tilanne vaatii kuitenkin sen, että ohjelman komponenttien tarjoamat rajapinnat eli metodit ja niiden parametrit on selkeästi määritelty.

Pohditaan uudestaan edellisen osan lopussa nähtyä kivi-paperi-sakset -peliä sekä siihen liittyvää "tekoälyä". Peli kysyy ensin käyttäjältä siirtoa, sitten tietokoneen valintaa, ja lopulta kertoo tuloksen. Pelin lopettaminen onnistuu nyt hahmottelemassamme versiossa syöttämällä tyhjän merkkijonon.

KPS!
Valitse [k]ivi, [p]aperi tai [s]akset: k
Tietokone valitsi: k
Tasapeli.
Tilanne: Pelaaja 0 - 0 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset: p
Tietokone valitsi: k
Pelaaja voitti!
Tilanne: Pelaaja 1 - 0 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset: p
Tietokone valitsi: k
Pelaaja voitti!
Tilanne: Pelaaja 2 - 0 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset: s
Tietokone valitsi: k
Tietokone voitti!
Tilanne: Pelaaja 2 - 1 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset: s
Tietokone valitsi: k
Tietokone voitti!
Tilanne: Pelaaja 2 - 2 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset: k
Tietokone valitsi: k
Tasapeli.
Tilanne: Pelaaja 2 - 2 Tietokone

Valitse [k]ivi, [p]aperi tai [s]akset:
Peli päättyi.
Lopputilanne: Pelaaja 2 - 2 Tietokone

Luodaan ohjelmaa varten neljä luokkaa: (1) tekstikäyttöliittymän toteuttamiseen käytettävä luokka Tekstikayttoliittyma, (2) pistetilanteen ylläpitoon käytettävä Pistetilanne, (3) Kivi-paperi-sakset -pelin sääntöjen ylläpitoon käytettävä luokka Saannot ja (4) tietokoneen tarjoaman vastustajan toimintaa kontrolloiva luokka Tekoaly.

Tietokoneen "tekoäly"

Tekoäly antaa siirron sovellukselle pyydettäessä, jonka lisäksi sille kerrotaan kierroksen lopussa vastustajan siirto. Ensimmäinen tekoäly voi olla hyvin yksinkertainen -- kun tekoäly luodaan erilliseen luokkaan, sitä voidaan jatkossa tarvittaessa parantaa. Luodaan luokasta Tekoaly sellainen, että se palauttaa aina kiven eikä tee tiedolla vastustajan aiemmista siirroista mitään.

Sovitaan, että ohjelmassa käytetään merkkijonoa "k" kiven kuvaamiseen, "p" paperin kuvaamiseen ja "s" saksien kuvaamiseen.

public class Tekoaly {

    public String annaSiirto() {
        return "k";
    }

    public void tallennaVastustajanSiirto(String siirto) {

    }
}

Pistetilanteen ylläpito

Pistetilanteen ylläpito tapahtuu luokan Pistetilanne avulla. Pistetilanne tallentaa sekä pelaajan että tekoälyn pisteet sekä mahdollistaa näiden pisteiden kasvattamisen. Tämän lisäksi pistetilanteen voi tulostaa toString-metodilla. Pistetilanne näyttää seuraavalta.

public class Pistetilanne {

    private int pelaajaPisteet;
    private int tietokonePisteet;

    public Pistetilanne() {
        this.pelaajaPisteet = 0;
        this.tietokonePisteet = 0;
    }

    public void pelaajaVoitti() {
        this.pelaajaPisteet++;
    }

    public void tietokoneVoitti() {
        this.tietokonePisteet++;
    }

    @Override
    public String toString() {
        return "Pelaaja " + this.pelaajaPisteet + " - " + this.tietokonePisteet + " Tietokone";
    }
}

Pelin säännöt

Hyödynnetään Wikipediaa Kivi, paperi ja sakset-pelin sääntöjen tarkastamiseen. Voitto tai tappio määritellään seuraavalla tavalla.

Pelaaja 1 Pelaaja 2 Voittaja
Kivi Sakset Pelaaja 1
Kivi Paperi Pelaaja 2
Kivi Kivi Tasapeli
Sakset Paperi Pelaaja 1
...

Luodaan sääntöjä varten luokka Saannot, joka palauttaa pelaajan voittaessa merkkijonon "pelaaja", tietokoneen voittaessa merkkijonon "tietokone", ja tasapelin yhteydessä merkkijonon "tasapeli".

public class Saannot {

    public String voittaja(String pelaajanValinta, String tietokoneenValinta) {
        if (pelaajanValinta.equals("k")) {
            if (tietokoneenValinta.equals("p")) {
                return "tietokone";
            }

            if (tietokoneenValinta.equals("s")) {
                return "pelaaja";
            }
        }

        if (pelaajanValinta.equals("p")) {
            if (tietokoneenValinta.equals("s")) {
                return "tietokone";
            }

            if (tietokoneenValinta.equals("k")) {
                return "pelaaja";
            }
        }

        if (pelaajanValinta.equals("s")) {
            if (tietokoneenValinta.equals("k")) {
                return "tietokone";
            }

            if (tietokoneenValinta.equals("p")) {
                return "pelaaja";
            }
        }

        return "tasapeli";
    }
}

Ylläolevan esimerkin voisi toteuttaa myös myöhemmin tutuksi tulevien enum-tyyppisten muuttujien avulla (eli järjestettävien ja ennalta määriteltyjen käsitteiden perusteella). Näitä käytettäessä käyttäisimme erillistä käsitettä Siirto, joka tietäisi miten se vertautuu toisiin siirtoihin. Vastaavasti käytössä olisi myös käsite Tulos, joka voisi olla esimerkiksi voitto, tappio tai tasapeli.

Tekstikäyttöliittymä

Nivotaan edelliset osat yhteen. Tekstikäyttöliittymä on luokka, joka sisältää sovelluslogiikkaan liittyvät oliot oliomuuttujina. Käytössämme olevilla sovelluslogiikan palasilla tekstikäyttöliittymän rakenne on seuraava:

public class Kayttoliittyma {
    // tekstikäyttöliittymän tarvitsemat oliomuuttujat
    private Scanner lukija;
    private Tekoaly tekoaly;
    private Saannot saannot;
    private Pistetilanne pistetilanne;

    // konstruktori, jolle annetaan parametrina tarvitut oliomuuttujat
    // ...

    // käyttöliittymän käynnistysmetodi
    public void kaynnista() {
        // tervehdysviesti

        // ohjelman toistuva logiikka
        while (true) {

            // syötteen lukeminen

            // lopetusehdon tarkastelu

            // logiikka
        }


        // lopetusviesti
    }
}

Hahmotellaan metodiin kaynnista käyttöliittymän tekstit.

public void kaynnista() {
    System.out.println("KPS!");
    System.out.println("");

    while (true) {
        System.out.print("Valitse [k]ivi, [p]aperi tai [s]akset: ");
        String pelaajanValinta = lukija.nextLine();

        if (/* lopetusehto */) {
            break;
        }

        String tietokoneenValinta = tekoaly.annaSiirto();
        System.out.println("Tietokone valitsi: " + tietokoneenValinta);

        tekoaly.tallennaVastustajanSiirto(pelaajanValinta);

        String voittaja = saannot.voittaja(pelaajanValinta, tietokoneenValinta);


        // kerro voittaja ja päivitä pisteet

        System.out.println("Tilanne: " + pistetilanne);
        System.out.println("");

    }

    System.out.println("Peli päättyi.");
    System.out.println("Lopputilanne: " + pistetilanne);
}

Lopetusehto on selkeä. Jos käyttäjä ei syötä merkkijonoa k, p tai s, toistosta poistutaan.

public boolean pelaajaHaluaaLopettaa(String pelaajanValinta) {
    return !pelaajanValinta.equals("k")
            && !pelaajanValinta.equals("p")
            && !pelaajanValinta.equals("s");
}

Voittajan kertomiseen ja pisteiden päivittämiseen käytetään sääntöjä ja pistetilannetta.

public void kerroVoittajaJaPaivitaPisteet(String voittaja) {
    if (voittaja.equals("tietokone")) {
        System.out.println("Tietokone voitti!");
        pistetilanne.tietokoneVoitti();
    } else if (voittaja.equals("pelaaja")) {
        System.out.println("Pelaaja voitti!");
        pistetilanne.pelaajaVoitti();
    } else {
        System.out.println("Tasapeli.");
    }
}

Kayttoliittyma on kokonaisuudessaan seuraavanlainen.

import java.util.Scanner;

public class Kayttoliittyma {

    private Scanner lukija;
    private Tekoaly tekoaly;
    private Saannot saannot;
    private Pistetilanne pistetilanne;

    public Kayttoliittyma(Scanner lukija, Tekoaly tekoaly, Saannot saannot, Pistetilanne pistetilanne) {
        this.lukija = lukija;
        this.tekoaly = tekoaly;
        this.saannot = saannot;
        this.pistetilanne = pistetilanne;
    }

    public void kaynnista() {
        System.out.println("KPS!");
        System.out.println("");

        while (true) {
            System.out.print("Valitse [k]ivi, [p]aperi tai [s]akset: ");
            String pelaajanValinta = lukija.nextLine();

            if (pelaajaHaluaaLopettaa(pelaajanValinta)) {
                break;
            }

            String tietokoneenValinta = tekoaly.annaSiirto();
            System.out.println("Tietokone valitsi: " + tietokoneenValinta);

            tekoaly.tallennaVastustajanSiirto(pelaajanValinta);

            String voittaja = saannot.voittaja(pelaajanValinta, tietokoneenValinta);

            kerroVoittajaJaPaivitaPisteet(voittaja);

            System.out.println("Tilanne: " + pistetilanne);
            System.out.println("");

        }

        System.out.println("Peli päättyi.");
        System.out.println("Lopputilanne: " + pistetilanne);
    }

    public void kerroVoittajaJaPaivitaPisteet(String voittaja) {
        if (voittaja.equals("tietokone")) {
            System.out.println("Tietokone voitti!");
            pistetilanne.tietokoneVoitti();
        } else if (voittaja.equals("pelaaja")) {
            System.out.println("Pelaaja voitti!");
            pistetilanne.pelaajaVoitti();
        } else {
            System.out.println("Tasapeli.");
        }
    }

    public boolean pelaajaHaluaaLopettaa(String pelaajanValinta) {
        return !pelaajanValinta.equals("k")
                && !pelaajanValinta.equals("p")
                && !pelaajanValinta.equals("s");
    }
}

Sovelluksen käynnistäminen onnistuu erillisestä Main-luokasta suoraviivaisesti.

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        // Testaa ohjelmasi toimintaa täällä!
        Scanner lukija = new Scanner(System.in);
        Pistetilanne pisteet = new Pistetilanne();
        Tekoaly tekoaly = new Tekoaly();
        Saannot saannot = new Saannot();

        Kayttoliittyma kayttis = new Kayttoliittyma(lukija, tekoaly, saannot, pisteet);
        kayttis.kaynnista();
    }
}

Pelin jatkokehitys

Sovellus toimii nyt halutulla tavalla. Jos ohjelmoija haluaisi muuttaa pelin sääntöjä -- esimerkiksi lisäämällä uusia siirtotyyppejä pelin Rock, Paper, Scissors, Lizard & Spock-hengessä, muutettavia kohtia on melko paljon, sillä siirtoa ei ole abstrahoitu omaksi käsitteekseen vaan se esitetään merkkijonona.

Toisaalta, tekoäly on kapseloitu sovelluksessa hyvin, jolloin sen jatkokehitys on suoraviivaista. Selkeä parannus edelliseen tekoälyyn olisi pelata aina sellainen siirto, joka voittaa pelaajan edellisen siirron. Tämä onnistuisi seuraavasti.

public class Tekoaly {
    private String vastustajanSiirto;

    public Tekoaly() {
        this.vastustajanSiirto = "k";
    }

    public String annaSiirto() {
        if (this.vastustajanSiirto.equals("k")) {
            return "p";
        } else if (this.vastustajanSiirto.equals("p")) {
            return "s";
        } else {
            return "k";
        }
    }

    public void tallennaVastustajanSiirto(String siirto) {
        this.vastustajanSiirto = siirto;
    }
}

Edellisen osan lopussa hahmoteltiin myös hieman parempaa tekoälyä..

Tehtävä vastaa kolmea yksiosaista tehtävää.

Tässä tehtävässä suunnittelet ja toteutat tietokannan lintubongareille. Tietokanta sisältää lintuja, joista jokaisella on nimi (merkkijono) ja latinankielinen nimi (merkkijono). Tämän lisäksi tietokanta laskee kunkin linnun havaintokertoja.

Ohjelmasi täytyy toteuttaa seuraavat komennot:

  • Lisaa - lisää linnun (huom: komennon nimessä ei ä-kirjainta!)
  • Havainto - lisää havainnon
  • Tilasto - tulostaa kaikki linnut
  • Nayta - tulostaa yhden linnun (huom: komennon nimessä ei ä-kirjainta!)
  • Lopeta - lopettaa ohjelman

Lisäksi virheelliset syötteet pitää käsitellä. (Ks. Simo alla). Tässä vielä esimerkki ohjelman toiminnasta:

? Lisaa
Nimi: Korppi
Latinankielinen nimi: Corvus Corvus
? Lisaa
Nimi: Haukka
Latinankielinen nimi: Dorkus Dorkus
? Havainto
Mikä havaittu? Haukka
? Havainto
Mikä havaittu? Simo
Ei ole lintu!
? Havainto
Mikä havaittu? Haukka
? Tilasto
Haukka (Dorkus Dorkus): 2 havaintoa
Korppi (Corvus Corvus): 0 havaintoa
? Nayta
Mikä? Haukka
Haukka (Dorkus Dorkus): 2 havaintoa
? Lopeta

Huom! Ohjelmasi rakenne on täysin vapaa. Testaamme vain että Paaohjelma luokan main-metodi toimii kuten tässä on kuvailtu. Hyödyt tehtävässä todennäköisesti ongelma-aluetta sopivasti kuvaavista luokista.

Ensiaskeleet automaattiseen testaamiseen

Otetaan seuraavaksi ensiaskeleet ohjelmien testaamiseen.

Virhetilanteet ja ongelman ratkaiseminen askel kerrallaan

Ohjelmia luodessa niihin päätyy virheitä. Joskus virheet eivät ole niin vakavia, ja aiheuttavat päänvaivaa lähinnä ohjelman käyttäjälle. Joskus toisaalta virheet voivat johtaa hyvinkin vakaviin seurauksiin. Varmaa on joka tapauksessa se, että ohjelmoimaan opetteleva ihminen tekee paljon virheitä.

Virheitä ei kannata missään nimessä pelätä tai välttää, sillä virheitä tekemällä oppii parhaiten. Pyri siis myös välillä rikkomaan työstämääsi ohjelmaa, jolloin pääset tutkimaan virheilmoitusta ja tarkastelemaan kertooko virheilmoitus jotain tekemästäsi virheestä.

Ohjelmistovirhe

Osoitteessa http://sunnyday.mit.edu/accidents/MCO_report.pdf oleva raportti kuvaa erään hieman vakavamman ohjelmistovirheestä johtuneen tapaturman sekä ohjelmistovirheen.

Ohjelmistovirhe liittyi siihen, että käytetty ohjelma odotti, että ohjelmoija käyttäisi kansainvälistä yksikköjärjestelmää laskuissa (metrit, kilogrammat, ...). Ohjelmoija oli kuitenkin käyttänyt amerikkalaista mittajärjestelmää erään järjestelmän osan laskuissa, jolloin satelliitin navigointiin liittynyt automaattinen korjausjärjestelmä ei toiminut oikein.

Satelliitti tuhoutui.

Ohjelmien muuttuessa monimutkaisemmiksi, tulee virheiden löytämisestäkin haastavampaa. NetBeansiin integroitu debuggeri voi olla avuksi virheiden löytämisessä. Debuggerin käyttöä on esitelty kurssimateriaaliin upotetuilla videoilla; alla oleva video esittelee myös miten projekteja voidaan luoda, avata ja sulkea sekä miten ohjelmia voidaan suorittaa NetBeansin ulkopuolella. Screencastissa on myös asioita, joita kurssilla ei vielä ole tullut -- älä huoli, nämä tulevat vastaan opintojen edetessä.

Stack trace

Kun ohjelmassa tapahtuu virhe, ohjelma tyypillisesti tulostaa ns. stack tracen, eli niiden metodikutsujen listan, joiden seurauksena virhetilanne syntyi. Stack trace voi näyttää esimerkiksi seuraavalta:

Exception in thread "main" ...
    at Ohjelma.main(Ohjelma.java:15)

Listan alussa kerrotaan minkälainen virhe tapahtui (tässä ...), ja seuraavalla rivillä kerrotaan missä virhe tapahtui. Rivi "at Ohjelma.main(Ohjelma.java:15)" sanoo, että virhe tapahtui Ohjelma.java-tiedoston rivillä 15.

at Ohjelma.main(Ohjelma.java:15)

Muistilista virheenselvitykseen

Jos koodisi ei toimi etkä tiedä missä on virhe, näillä askeleilla pääset alkuun.

  1. Sisennä koodisi oikein ja selvitä, puuttuuko sulkuja.
  2. Tarkista ovatko käytetyt muuttujat oikean nimisiä.
  3. Testaa ohjelman kulkua erilaisilla syötteillä, ja selvitä minkälaisella syötteellä ohjelma ei toimi halutusti. Jos sait virheen testeistä, testit saattavat myös kertoa käytetyn syötteen.
  4. Lisää ohjelmaan tulostuskomentoja, joissa tulostat käytettävien muuttujien arvoja ohjelman suorituksen eri vaiheissa.
  5. Tarkista, että kaikki käyttämäsi muuttujat on alustettu. Jos tätä ei ole tehty, seuraa virhe NullPointerException.
  6. Jos ohjelmasi aiheuttaa poikkeuksen, kannattaa ehdottomasti kiinnittää huomiota poikkeuksen yhteydessä olevaan stack traceen, eli niiden metodikutsujen listaan, minkä seurauksena poikkeuksen aiheuttanut tilanne syntyi.
  7. Opettele käyttämään debuggeria, aiemmin nähdyllä videolla pääsee alkuun.

Testisyötteen antaminen Scannerille

Ohjelman testaaminen käsin on usein työlästä. Syötteen antaminen on mahdollista automatisoida esimerkiksi syöttämällä Scanner-oliolle luettava merkkijono. Alla on annettu esimerkki siitä, miten ohjelmaa voi testata automaattisesti. Ohjelmassa syötetään ensin viisi merkkijonoa, jonka jälkeen syötetään aiemmin nähty merkkijono. Tämän jälkeen yritetään syöttää vielä uusi merkkijono. Merkkijonoa "kuusi" ei pitäisi esiintyä sanajoukossa.

Testisyötteen voi antaa merkkijonona Scanner-oliolle konstruktorissa. Jokainen testisyötteessä annettava rivinvaihto merkitään merkkijonoon kenoviivan ja n-merkin yhdistelmänä "\n".

String syote = "yksi\n" + "kaksi\n"  +
               "kolme\n" + "nelja\n" +
               "viisi\n" + "yksi\n"  +
               "kuusi\n";

Scanner lukija = new Scanner(syote);

ArrayList<String> luetut = new ArrayList<>();

while (true) {
    System.out.println("Anna syote: ");
    String rivi = lukija.nextLine();
    if (luetut.contains(rivi)) {
        break;
    }

    luettu.add(rivi);
}

System.out.println("Kiitos!");

if (luetut.sisaltaa("kuusi")) {
    System.out.println("Joukkoon lisättiin arvo, jota sinne ei olisi pitänyt lisätä.");
}

Ohjelma tulostus näyttää vain ohjelman antaman tulostuksen, ei käyttäjän tekemiä komentoja.

Anna syote:
Anna syote:
Anna syote:
Anna syote:
Anna syote:
Anna syote:
Kiitos!

Merkkijonon antaminen Scanner-luokan konstruktorille korvaa näppäimistöltä luettavan syötteen. Merkkijonomuuttujan syote sisältö siis "simuloi" käyttäjän antamaa syötettä. Rivinvaihto syötteeseen merkitään \n:llä. Jokainen yksittäinen rivinvaihtomerkkiin loppuva osa syote-merkkijonossa siis vastaa yhtä nextLine()-komentoon annettua syötettä.

Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in, eli järjestelmän syötevirta. Voit toisaalta halutessasi myös vaihtaa testisyötettä, sillä kyse on merkkijonosta.

Ohjelman toiminnan oikeellisuus tulee edelleen tarkastaa ruudulta. Tulostus voi olla aluksi hieman hämmentävää, sillä automatisoitu syöte ei näy ruudulla ollenkaan. Lopullinen tavoite on automatisoida myös ohjelman tulostuksen oikeellisuden tarkastaminen niin hyvin, että ohjelman testaus ja testituloksen analysointi onnistuu "nappia painamalla". Palaamme aiheeseen myöhemmissä osissa.

Yksikkötestaus

Edellä esitetty menetelmä automaattiseen testaamiseen missä ohjelmalle syötetyt syötteet muutetaan on varsin kätevä, mutta kuitenkin melko rajoittunut. Isompien ohjelmien testaaminen edellä kuvatulla tavalla on haastavaa. Eräs ratkaisu tähän on yksikkötestaus, missä ohjelman pieniä osia testataan erikseen.

Yksikkötestauksella tarkoitetaan lähdekoodiin kuuluvien yksittäisten osien kuten luokkien ja niiden tarjoamien metodien testaamista. Luokkien ja metodien rakenteen suunnittelussa käytettävän ohjesäännön -- jokaisella metodilla ja luokalla tulee olla yksi selkeä vastuu -- noudattamisen tai siitä poikkeamisen huomaa testejä kirjoittaessa. Mitä useampi vastuu metodilla on, sitä monimutkaisempi testi on. Jos laaja sovellus on kirjoitettu yksittäiseen metodiin, on testien kirjoittaminen sitä varten erittäin haastavaa ellei jopa mahdotonta. Vastaavasti, jos sovellus on pilkottu selkeisiin luokkiin ja metodeihin, on testienkin kirjoittaminen suoraviivaista.

Testien kirjoittamisessa hyödynnetään tyypillisesti valmiita yksikkötestauskirjastoja, jotka tarjoavat metodeja ja apuluokkia testien kirjoittamiseen. Javassa käytetyin yksikkötestauskirjasto on JUnit, johon löytyy myös tuki lähes kaikista ohjelmointiympäristöistä. Esimerkiksi NetBeans osaa automaattisesti etsiä JUnit-testejä projektista -- jos testejä löytyy, ne näytetään projektin alla Test Packages -kansiossa.

Tarkastellaan yksikkötestien kirjoittamista esimerkin kautta. Oletetaan, että käytössämme on seuraava luokka Laskin, ja haluamme kirjoittaa sitä varten automaattisia testejä.

public class Laskin {

    private int arvo;
 
    public Laskin() {
        this.arvo = 0;
    }

    public void summa(int luku) {
        this.arvo += luku;
    }

    public void erotus(int luku) {
        this.arvo += luku;
    }

    public int getArvo() {
        return this.arvo;
    }
}

Laskimen toiminta perustuu siihen, että se muistaa aina edellisen laskuoperaation tuottaman tuloksen. Seuraavat laskuoperaatiot lisätään aina edelliseen lopputulokseen. Yllä olevaan laskimeen on jäänyt myös pieni copy-paste -ohjelmoinnista johtuva virhe. Metodin erotus pitäisi vähentää arvosta, mutta nyt se lisää arvoon.

Yksikkötestien kirjoittaminen aloitetaan testiluokan luomisella. Testiluokka luodaan Test Packages -kansion alle. Kun testaamme luokkaa Laskin, testiluokan nimeksi tulee LaskinTest. Nimen lopussa oleva merkkijono Test kertoo ohjelmointiympäristölle, että kyseessä on testiluokka. Ilman merkkijonoa Test luokassa olevia testejä ei suoriteta. (Huom! Testit luodaan NetBeansissa Test Packages -kansion alle.)

Testiluokka LaskinTest on aluksi tyhjä.

public class LaskinTest {

}

Testit ovat testiluokassa olevia metodeja ja jokainen testi testaa yksittäistä asiaa. Aloitetaan luokan Laskin testaaminen -- luodaan ensin testimetodi, jossa varmistetaan, että juuri luodun laskimen sisältämä arvo on 0.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class LaskinTest {

    @Test
    public void laskimenArvoAlussaNolla() {
        Laskin laskin = new Laskin();
        assertEquals(0, laskin.getArvo());
    }
}

Yllä olevassa metodissa laskimenArvoAlussaNolla luodaan ensin laskinolio. Tämän jälkeen käytetään JUnit-testikehyksen tarjoamaa assertEquals-metodia arvon tarkistamiseen. Metodi tuodaan luokasta Assert komennolla import static, ja sille annetaan parametrina odotettu arvo -- tässä 0 -- sekä laskimen palauttama arvo. Jos metodin assertEquals arvot poikkeavat toisistaan, testin suoritus ei pääty hyväksytysti. Jokaisella testimetodilla tulee olla annotaatio @Test -- tämä kertoo JUnit-testikehykselle, että kyseessä on suoritettava testimetodi.

Testien suorittaminen onnistuu valitsemalla projekti oikealla hiirennapilla ja klikkaamalla vaihtoehtoa Test.

Testien suorittaminen luo output-välilehdelle (tyypillisesti NetBeansin alalaidassa) tulosteen, jossa on testiluokkakohtaiset tilastot. Alla olevassa esimerkissä on suoritettu pakkauksessa laskin olevan testiluokan LaskinTest testit. Testejä suoritettiin 1, joista yksikään ei epäonnistunut -- epäonnistuminen tarkoittaa tässä sitä, että testin testaama toiminnallisuus ei toiminut oletetusti.

Testsuite: LaskinTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.054 sec

test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Lisätään testiluokkaan summaa ja erotusta lisäävää toiminnallisuutta.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class LaskinTest {

    @Test
    public void laskimenArvoAlussaNolla() {
        Laskin laskin = new Laskin();
        assertEquals(0, laskin.getArvo());
    }

    @Test
    public void arvoViisiKunSummataanViisi() {
        Laskin laskin = new Laskin();
        laskin.summa(5);
        assertEquals(5, laskin.getArvo());
    }

    @Test
    public void arvoMiinusKaksiKunErotetaanKaksi() {
        Laskin laskin = new Laskin();
        laskin.erotus(2);
        assertEquals(-2, laskin.getArvo());
    }
}

Testien suorittaminen antaa seuraavanlaisen tulostuksen.

Testsuite: LaskinTest
Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.059 sec

Testcase: arvoMiinusKaksiKunErotetaanKaksi(LaskinTest):	FAILED
expected:<-2> but was:<2>
junit.framework.AssertionFailedError: expected:<-2> but was:<2>
    at LaskinTest.arvoMiinusKaksiKunErotetaanKaksi(LaskinTest.java:25)


Test LaskinTest FAILED
test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Tulostus kertoo, että kolme testiä suoritettiin. Yksi niistä päätyi virheeseen. Testitulostuksessa on tieto myös testin rivistä, jossa virhe tapahtui (25) sekä tieto odotetusta (-2) ja saadusta arvosta (2). Kun testien suoritus päättyy virheeseen, NetBeans näyttää testien suoritukseen liitttyvän virhetilanteen myös visuaalisena.

Edellisillä testeillä kaksi testeistä menee läpi, mutta yhdessä on tapahtunut virhe. Korjataan luokkaan Laskin jäänyt virhe.

// ...
public void erotus(int luku) {
    this.arvo -= luku;
}
// ...

Kun testit suoritetaan uudestaan, testit menevät läpi.

Testsuite: LaskinTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 sec

test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Testivetoinen ohjelmistokehitys

Testivetoinen ohjelmistokehitys (Test-driven development) on ohjelmistokehitysprosessi, joka perustuu ohjelman rakentamiseen pienissä osissa. Testivetoisessa ohjelmistokehityksessä ohjelmoija kirjoittaa aina ensin automaattisesti suoritettavan yksittäistä tietokoneohjelman osaa tarkastelevan testin.

Testi ei mene läpi, sillä testin täyttävä toiminnallisuus eli tarkasteltava tietokoneohjelman osa puuttuu. Kun testi on kirjoitettu, ohjelmaan lisätään toiminnallisuus, joka täyttää testin vaatimukset. Testit suoritetaan uudestaan, jonka jälkeen -- jos kaikki testit menevät läpi -- lisätään uusi testi tai vaihtoehtoisesti -- jos testit eivät mene läpi -- korjataan aiemmin kirjoitettua ohjelmaa. Ohjelman sisäistä rakennetta korjataan eli refaktoroidaan tarvittaessa siten, että ohjelman toiminnallisuus pysyy samana mutta rakenne selkiytyy.

Testivetoinen ohjelmistokehitys koostuu viidestä askeleesta, joita toistetaan kunnes ohjelman toiminnallisuus on valmis.

  • Kirjoita testi. Ohjelmoija päättää, mitä ohjelman toiminnallisuutta testataan, ja kirjoittaa toiminnallisuutta varten testin.
  • Suorita testit ja tarkista menevätkö testit läpi. Kun uusi testi on kirjoitettu, testit suoritetaan. Jos testin suoritus päättyy hyväksyttyyn tilaan, testissä on todennäköisesti virhe ja se tulee korjata -- testin pitäisi testata vain toiminnallisuutta, jota ei ole vielä toteutettu.
  • Kirjoita toiminnallisuus, joka täyttää testin vaatimukset. Ohjelmoija toteuttaa toiminnallisuuden, joka täyttää vain testin vaatimukset. Huomaa, että tässä ei toteuteta asioita, joita testi ei vaadi -- toiminnallisuutta lisätään vain vähän kerrallaan.
  • Suorita testit. Jos testit eivät pääty hyväksyttyyn tilaan, kirjoitetussa toiminnallisuudessa on todennäköisesti virhe. Korjaa toiminnallisuus -- tai, jos toiminnallisuudessa ei ole virhettä -- korjaa viimeksi toteutettu testi.
  • Korjaa ohjelman sisäistä rakennetta. Kun ohjelman koko kasvaa, sen sisäistä rakennetta korjataan tarvittaessa. Liian pitkät metodit pilkotaan useampaan osaan ja ohjelmasta eriytetään käsitteisiin liittyviä luokkia. Testejä ei muuteta, vaan niitä hyödynnetään ohjelman sisäiseen rakenteeseen tehtyjen muutosten oikeellisuuden varmistamisessa -- jos ohjelman rakenteeseen tehty muutos muuttaa ohjelman toiminnallisuutta, testit varoittavat siitä, ja ohjelmoija voi korjata tilanteen.

Testiluokka ja ensimmäinen testi

Tarkastellaan tätä prosessia tehtävien hallintaan tarkoitetun sovelluksen kannalta. Tehtävien hallintasovellukseen halutaan mahdollisuus tehtävien listaamiseen, lisäämiseen, tehdyksi merkkaamiseen sekä poistamiseen. Aloitetaan sovelluksen kehitys luomalla tyhjä testiluokka. Asetetaan testiluokan nimeksi TehtavienHallintaTest, ja lisätään se pakkaukseen tehtavat. Tällä hetkellä sovelluksessa ei ole vielä lainkaan toiminnallisuutta.

 

Luodaan ensimmäinen testi. Testissä määritellään luokka Tehtavienhallinta, ja oletetaan, että luokalla on metodi tehtavalista, joka palauttaa tehtävälistan. Testimetodi assertEquals saa parametrinaan kaksi arvoa -- ensimmäinen on odotettu arvo ja toinen on ohjelman palauttama arvo. Tässä metodia käytetään tehtävälistan koon tarkastamiseen uuden tehtävälistan luomisen yhteydessä: uuden listan tulee olla tyhjä.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class TehtavienhallintaTest {

    @Test
    public void tehtavalistaAlussaTyhja() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        assertEquals(0, hallinta.tehtavalista().size());
    }
}

Luokkaa Tehtavienhallinta ei ole määritelty joten testien suoritus epäonnistuu.

Ensimmäisen testin vaatimusten täyttäminen

Toteutetaan seuraavaksi toiminnallisuus, joka täyttää testin. Luodaan luokka Tehtavienhallinta ja lisätään luokalle toiminnallisuus, joka täyttää testin vaatimukset. Luokka luodaan NetBeansissa kansioon Source Packages. Nyt projekti näyttää seuraavalta.

 

Toiminnallisuus on yksinkertainen. Luokalla Tehtavienhallinta on metodi tehtavalista, joka palauttaa tyhjän listan.

import java.util.ArrayList;

public class Tehtavienhallinta {

    public ArrayList<String> tehtavalista() {
        return new ArrayList<>();
    }
}

Testit menevät läpi. Luokan Tehtavienhallinta sisäinen rakenne on vielä niin pieni, ettei siinä ole juurikaan korjattavaa.

Toinen testi

Aloitamme testivetoiseen kehitykseen liittyvän syklin uudestaan. Seuraavaksi luomme uuden testin, jossa tarkastellaan tehtävien lisäämiseen liittyvää toiminnallisuutta. Testissä määritellään luokalle Tehtavienhallinta metodi lisää, joka lisää tehtävälistalle uuden tehtävän. Tehtävän lisäämisen onnistuminen tarkastetaan tehtavalista-metodin koon kasvamisen kautta.

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class TehtavienhallintaTest {

    @Test
    public void tehtavalistaAlussaTyhja() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        assertEquals(0, hallinta.tehtavalista().size());
    }

    @Test
    public void tehtavanLisaaminenKasvattaaListanKokoaYhdella() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        hallinta.lisaa("Kirjoita testi");
        assertEquals(1, hallinta.tehtavalista().size());
    }
}

Testi ei toimi lainkaan, sillä luokasta Tehtavienhallinta puuttuu lisaa-metodi.

Toisen testin vaatimusten täyttäminen

Lisätään luokkaan Tehtavienhallinta metodi lisaa, ja suoritetaan testit.

import java.util.ArrayList;

public class Tehtavienhallinta {

    public ArrayList<String> tehtavalista() {
        return new ArrayList<>();
    }

    public void lisaa(String tehtava) {

    }
}

Nyt testien ajamisesta saadaan seuraava ilmoitus.

Testsuite: TehtavienhallintaTest
Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.053 sec

Testcase: tehtavanLisaaminenKasvattaaListanKokoaYhdella(TehtavienhallintaTest):	FAILED
    expected:<1> but was:<0>
    junit.framework.AssertionFailedError: expected:<1> but was:<0>
    at TehtavienhallintaTest.tehtavanLisaaminenKasvattaa...(TehtavienhallintaTest.java:18)

Testit eivät siis mene vieläkään läpi. Muokataan luokan tehtävänhallinta toiminnallisuutta siten, että luokalle luodaan oliomuuttujaksi tehtävät sisältävä lista. Muokataan metodin lisaa-toiminnallisuutta vain niin, että se läpäisee testin, mutta ei tee todellisuudessa haluttua asiaa.

import java.util.ArrayList;

public class Tehtavienhallinta {

    private ArrayList<String> tehtavat;

    public Tehtavienhallinta() {
        this.tehtavat = new ArrayList<>();
    }

    public ArrayList<String> tehtavalista() {
        return this.tehtavat;
    }

    public void lisaa(String tehtava) {
        this.tehtavat.add("Uusi");
    }
}

Testit menevät läpi, joten olemme tyytyväisiä ja voimme siirtyä seuraavaan askeleeseen.

Testsuite: TehtavienhallintaTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 sec

test-report:
test:
BUILD SUCCESSFUL (total time: 0 seconds)

Kolmas testi

Täydennetään testejä siten, että ne vaativat, että lisätyn tehtävän tulee olla listalla. JUnit-kirjaston tarjoama metodi assertTrue vaatii, että sille parametrina annettu lauseke saa lopulta arvon true.

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class TehtavienhallintaTest {

    @Test
    public void tehtavalistaAlussaTyhja() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        assertEquals(0, hallinta.tehtavalista().size());
    }

    @Test
    public void tehtavanLisaaminenKasvattaaListanKokoaYhdella() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        hallinta.lisaa("Kirjoita testi");
        assertEquals(1, hallinta.tehtavalista().size());
    }

    @Test
    public void lisattyTehtavaLoytyyTehtavalistalta() {
        Tehtavienhallinta hallinta = new Tehtavienhallinta();
        hallinta.lisaa("Kirjoita testi");
        assertTrue(hallinta.tehtavalista().contains("Kirjoita testi"));
    }
}

Testit eivät mene taaskaan läpi ja ohjelman toiminnallisuutta tulee muokata.

Kolmannen testin vaatimusten täyttäminen

Noheva ohjelmoija muokkaisi luokan Tehtavienhallinta toimintaa siten, että metodissa lisaa lisättäisiin listalle aina merkkijono "Kirjoita testi". Tämä johtaisi tilanteeseen, missä testit menisivät läpi, mutta toiminnallisuus sovellus ei vieläkään tarjoaisi toimivaa tehtävien lisäämistoiminnallisuutta. Muokataan luokkaa Tehtavienhallinta siten, että lisättävä tehtävä lisätään tehtävälistalle.

import java.util.ArrayList;

public class Tehtavienhallinta {

    private ArrayList<String> tehtavat;

    public Tehtavienhallinta() {
        this.tehtavat = new ArrayList<>();
    }

    public List<String> tehtavalista() {
        return this.tehtavat;
    }

    public void lisaa(String tehtava) {
        this.tehtavat.add(tehtava);
    }
}

Nyt testit menevät taas läpi.

Testien refaktorointi

Huomaamme, että testiluokassa on taas jonkinverran toistoa -- siirretään Tehtavienhallinta testiluokan oliomuuttujaksi, ja alustetaan se jokaisen testin alussa. Metodi alusta suoritetaan ennen jokaisen testimetodin suoritusta. Tämä tapahtuu koska metodille on määritelty ohjeiden antamiseen tarkoitettu annotaatio @Before.

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;

public class TehtavienhallintaTest {

    private Tehtavienhallinta hallinta;

    @Before
    public void alusta() {
        hallinta = new Tehtavienhallinta();
    }

    @Test
    public void tehtavalistaAlussaTyhja() {
        assertEquals(0, hallinta.tehtavalista().size());
    }

    @Test
    public void tehtavanLisaaminenKasvattaaListanKokoaYhdella() {
        hallinta.lisaa("Kirjoita testi");
        assertEquals(1, hallinta.tehtavalista().size());
    }

    @Test
    public void lisattyTehtavaLoytyyTehtavalistalta() {
        hallinta.lisaa("Kirjoita testi");
        assertTrue(hallinta.tehtavalista().contains("Kirjoita testi"));
    }
}

Neljäs testi

Lisätään seuraavaksi mahdollisuus tehtävän tehdyksi merkkaamiseen. Mutta! Mitä tarkoittaa tehdyksi merkkaaminen? Alunperin tavoitteena oli luoda ohjelma, joka mahdollistaa tehtävien listaamisen, listaamisen, tehdyksi merkkaamisen sekä poistamisen. Miten tarkastamme onko tehtävä tehty? Jos emme voi tietää onko tehtävä tehty vai ei, voisimme periaatteessa jättää koko toiminnallisuuden huomiotta. Voimme toisaalta päättää miten tehtän tehdyksi määrittely tapahtuu.

Määritellään ensin testi, joka mahdollistaa tehtävän tehdyksi merkkaamiseen.

// ...
@Test
public void tehtavanVoiMerkataTehdyksi() {
    hallinta.lisaa("Satunnainen tehtava");
    hallinta.merkkaaTehdyksi("Satunnainen tehtava");
}
// ..

Neljännen testin vaatimusten täyttäminen

Tehtavienhallintaan lisätään seuraavaksi metodi merkkaaTehdyksi. Metodin toiminnallisuus voi olla aluksi tyhjä, sillä testi vaatii vain kyseisen metodin olemassaolon.

Viides testi

Lisätään tämän jälkeen testi, jonka tehtävänä on tarkistaa onko parametrina annettu tehtävä tehty.

// ...
@Test
public void tehdyksiMerkattuOnTehty() {
    hallinta.lisaa("Uusi tehtava");
    hallinta.merkkaaTehdyksi("Uusi tehtava");
    assertTrue(hallinta.onTehty("Uusi tehtava"));
}
// ..

Viidennen testin vaatimusten täyttäminen

Nyt toiminnallisuutta varten tulee toteuttaa uusi metodi onTehty. Metodi voi aluksi palauttaa aina arvon true. Kokko luokan Tehtavienhallinta sisältö on nyt seuraava.

import java.util.ArrayList;

public class Tehtavienhallinta {

    private ArrayList<String> tehtavat;

    public Tehtavienhallinta() {
        this.tehtavat = new ArrayList<>();
    }

    public ArrayList<String> tehtavalista() {
        return this.tehtavat;
    }

    public void lisaa(String tehtava) {
        this.tehtavat.add(tehtava);
    }

    public void merkkaaTehdyksi(String tehtava) {

    }

    public boolean onTehty(String tehtava) {
        return true;
    }
}

Testit menevät taas läpi.

Kuudes testi

Seuraavaksi toteutettava testi on oleellinen tehtävän toiminnan kannalta. Olemme tähän mennessä tarkistaneet, että haluttu toiminnallisuus on olemassa, mutta emme ole juurikaan tarkastaneet epätoivotun toiminnan poissaoloa. Jos testejä kirjoitettaessa keskitytään halutun toiminnallisuuden olemassaoloon, testit saattavat jäädä ohjelman toiminnallisuutta hyvin vähän tarkastelevaksi.

Kirjoitetaan seuraavaksi testi, joka tarkastaa, että tekemättömäksi merkkaamaton testi ei ole tehty.

// ...
@Test
public void tehdyksiMerkkaamatonEiOleTehty() {
    hallinta.lisaa("Uusi tehtava");
    hallinta.merkkaaTehdyksi("Uusi tehtava");
    assertFalse(hallinta.onTehty("Joku tehtava"));
}
// ..

Kuudennen testin vaatimusten täyttäminen

Joudumme nyt muokkaamaan luokan Tehtavienhallinta toiminnallisuutta hieman enemmän. Lisätään luokkaan erillinen lista tehtäville, jotka on merkattu tehdyiksi.

import java.util.ArrayList;

public class Tehtavienhallinta {

    private ArrayList<String> tehtavat;
    private ArrayList<String> tehdytTehtavat;

    public Tehtavienhallinta() {
        this.tehtavat = new ArrayList<>();
        this.tehdytTehtavat = new ArrayList<>();
    }

    public List<String> tehtavalista() {
        return this.tehtavat;
    }

    public void lisaa(String tehtava) {
        this.tehtavat.add(tehtava);
    }

    public void merkkaaTehdyksi(String tehtava) {
        this.tehdytTehtavat.add(tehtava);
    }

    public boolean onTehty(String tehtava) {
        return this.tehdytTehtavat.contains(tehtava);
    }
}

Testit menevät taas läpi. Sovelluksessa on muutamia muitakin kysymysmerkkejä. Pitäisikö tehtavalistauksessa palautetut tehtävät merkitä jollain tavalla tehdyksi? Voiko tehtävän, joka ei ole tehtävälistalla tosiaankin merkata tehdyksi?

Refaktorointi ja käsite "Tehtävä"

Tehdään ensimmäinen hieman laajempi ohjelman sisäisen rakenteen korjaus. Tehtävä on selkeästi käsite, joten sille kannattanee luoda oma erillinen luokka. Luodaan luokka Tehtava. Luokalla Tehtava on nimi sekä tieto siitä, onko tehtävä tehty.

public class Tehtava {

    private String nimi;
    private boolean tehty;

    public Tehtava(String nimi) {
        this.nimi = nimi;
        this.tehty = false;
    }

    public String getNimi() {
        return nimi;
    }

    public void setTehty(boolean tehty) {
        this.tehty = tehty;
    }

    public boolean onTehty() {
        return tehty;
    }
}

Muokataan tämän jälkeen luokan Tehtavienhallinta sisäistä rakennetta siten, että luokka tallentaa tehtävät merkkijonojen sijaan Tehtava-olioina. Huomaa, että luokan metodien määrittelyt eivät muutu, mutta niiden sisäinen toteutus muuttuu.

import java.util.ArrayList;

public class Tehtavienhallinta {

    private ArrayList<Tehtava> tehtavat;

    public Tehtavienhallinta() {
        this.tehtavat = new ArrayList<>();
    }

    public ArrayList<String> tehtavalista() {
        ArrayList<String> palautettavat = new ArrayList<>();
        for (Tehtava tehtava: tehtavat) {
            palautettavat.add(tehtava.getNimi());
        }

        return palautettavat;
    }

    public void lisaa(String tehtava) {
        this.tehtavat.add(new Tehtava(tehtava));
    }

    public void merkkaaTehdyksi(String tehdyksiMerkattavaTehtava) {
        for (Tehtava tehtava: tehtavat) {
            if (tehtava.getNimi().equals(tehdyksiMerkattavaTehtava)) {
                tehtava.setTehty(true);
            }
        }
    }

    public boolean onTehty(String tarkistettavaTehtava) {
        for (Tehtava tehtava: tehtavat) {
            if (tehtava.getNimi().equals(tarkistettavaTehtava) && tehtava.onTehty()) {
                return true;
            }
        }

        return false;
    }
}

Vaikka tehty muutos muutti luokan Tehtavienhallinta sisäistä toimintaa merkittävästi, testit toimivat yhä. Sykli jatkuisi samalla tavalla kunnes toivottu perustoiminnallisuus olisi paikallaan.

Tehtäväpohjassa tulee edellisen esimerkin alkutilanne. Seuraa edellistä esimerkkiä, ja luo Tehtavienhallinnalta haluttu toiminnallisuus testivetoista ohjelmistokehitystä noudattaen. Kun olet saanut edellisen esimerkin loppuun asti, lisää sovellukseen vielä testit tehtävien poistamiseen sekä testien vaatima toiminnallisuus.

Kohdat on pisteytetty askeleittain, jotka ovat seuraavat:

  1. Testiluokka ja ensimmäinen testi, ensimmäisen testin vaatimusten täyttäminen. Toinen testi, toisen testin vaatimusten täyttäminen.
  2. Kolmas testi, kolmannen testin vaatimusten täyttäminen, testien refaktorointi. Neljäs testi, neljännen testin vaatimusten täyttäminen, viides testi, viidennen testin vaatimusten täyttäminen.
  3. Kuudes testi, kuudennen testin vaatimusten täyttäminen. Refaktorointi ja käsitteen tehtävä eristäminen. Tehtävien poistamiseen liittyvät testit sekä toiminnallisuus -- toteuta poistaminen Tehtavienhallinta-luokkaan metodina public void poista(String tehtava)

Sitä mukaa kun kehität toiminnallisuutta, päivitä luokan Ohjelma metodia osiaToteutettu palauttamaan valmiiksi saamasi osan numero. Voit palauttaa tehtävän vaikket tekisikään kaikkia osia, jolloin saat pisteitä tehtävän niistä osista, jotka olet tehnyt.

Esimerkiksi, kun olet saanut ensimmäiset kaksi testiä sekä niihin liittyvän toiminnallisuuden toimimaan olet vaiheessa 1, jolloin metodin osiaToteutettu tulisi palautta arvo 1.

Lisää ohjelmistojen testaamisesta

Yksikkötestaus on vain osa ohjelmiston testaamista. Yksikkötestaamisen lisäksi ohjelmiston toteuttaja toteuttaa myös integraatiotestejä, joissa tarkastellaan komponenttien kuten luokkien yhteistoiminnallisuutta, sekä käyttöliittymätestejä, joissa testataan sovelluksen käyttöliittymää käyttöliittymän tarjoamien elementtien kuten nappien kautta.

Näitä testaamiseen liittyviä menetelmiä tarkastellaan tarkemmin muunmuassa kursseilla ohjelmistotekniikan menetelmät sekä ohjelmistotuotanto.

Crowdsorcerer: Arvioi tehtäviä

Otetaan hetkeksi askel taaksepäin ja muistellaan hajautustaulujen käyttöä. Tämän jälkeen tutustumme listojen ja hajautustaulujen käyttöä hajautustaulun arvona.

Ohjelmointikurssin kuudennessa osassa loimme taas omia tehtäviä Crowdsorcererin avulla. Nyt on hetki vertaisarviointiin -- arvioimme Crowdsorcereriin lähetettyjä tehtäviä! Anna vertaispalautetta kahdesta jonkun toisen kurssilaisen lähettämästä tehtävästä ja arvioi lopuksi itse tekemääsi tehtävää. Itse tekemäsi tehtävä näkyy vain jos olet tehnyt sen -- jos et tehnyt tehtävää, pääset arvioimaan yhden ylimääräisen tehtävän.

Vertaisarviointi

Alla on kolme Crowdsorcereriin tehtyä tehtävää: kaksi jonkun kurssitoverisi lähettämää ja yksi itsearviointia varten. Niiden yhteydessä on muistin virkistykseksi ohjeistus, jonka pohjalta kyseiset tehtävänannot on tehty.

Tarkastele jokaisen tehtävän eri osia: tehtävänantoa, tehtäväpohjaa ja malliratkaisua sekä testaukseen käytettäviä syötteitä ja tulosteita. Arvioi niiden selkeyttä, vaikeutta ja sitä, kuinka hyvin ne vastaavat ohjeistukseensa.

Voit vaihtaa näkymää tehtäväpohjan ja mallivastauksen välillä painamalla lähdekoodin yläpalkin painikkeita. Palautteenannon avuksi on annettu väittämiä. Voit valita kuinka samaa mieltä niiden kanssa olet painamalla hymiöitä. Annathan myös sanallista palautetta sille varattuun kenttään! Lisää vielä tehtävää mielestäsi kuvaavia tageja ja paina Lähetä.

Anna arvio kummallekin vertaispalautetehtävälle ja lopuksi vielä omallesi.

Muista olla reilu ja ystävällinen. Hyvä palaute on rehellistä, mutta kannustavaa!

Voit halutessasi ladata arvioitavan tehtävän tehtäväpohjan ja malliratkaisun koneellesi, ja testata niiden käyttöä. Molemmat tulevat ZIP-paketeissa, jolloin sinun täytyy purkaa ne, ennen kuin voit avata ne NetBeansissä.

Suunnittele oma tehtävä: hajautustaulu

Keksi tehtävä, jossa käytetään HashMappia. Tehtäväpohjassa on valmiina komennon kysyminen ja toistolause, joka jatkuu kunnes ohjelman käyttäjä kirjoittaa komennon "lopeta".

Huom! Tässä sinun täytyy syöttää jokaiselle testitapaukselle useampi syöte. Useamman syötteen saat annettua, kun laitat rivinvaihdon \n jokaisen syötteen väliin. Lisäksi lopeta jokainen testisyöte tekstillä lopeta, jotta testissä silmukan suoritus lakkaa.

Esimerkiksi jos haluat antaa testisyötteeksi "kissa", "koira", "lopeta", syötä input-kenttään teksti kissa\nkoira\nlopeta.

Muista merkitä malliratkaisurivit ohjelmaan -- näin ratkaisu ei tule suoraan käyttäjälle näkyvään.

Tiedon ryhmittely hajautustaulun avulla

Hajautustaulu sisältää korkeintaan yhden arvon yhtä avainta kohti. Seuraavassa esimerkissä tallennamme henkilöiden puhelinnumeroita hajautustauluun.

HashMap<String, String> puhelinnumerot = new HashMap<>();
puhelinnumerot.put("Pekka", "040-12348765");

System.out.println("Pekan numero: " + puhelinnumerot.get("Pekka"));

puhelinnumerot.put("Pekka", "09-111333");

System.out.println("Pekan numero: " + puhelinnumerot.get("Pekka"));
Pekan numero: 040-12348765
Pekan numero: 09-111333

Entä jos haluaisimme liittää yhteen avaimeen useita arvoja, eli esimerkiksi useampia puhelinnumeroita yhdelle henkilölle?

Koska hajautustaulun avaimet ja arvot voivat olla mitä tahansa muuttujia, listojen käyttäminen hajautustaulun arvona onnistuu. Useamman arvon lisääminen yhdelle arvolle onnistuu liittämällä avaimeen lista. Muutetaan puhelinnumeroiden talletustapaa seuraavasti:

HashMap<String, ArrayList<String>> puhelinnumerot = new HashMap<>();

Nyt hajautustaulussa on jokaiseen avaimeen liitettynä lista. Vaikka new-komento luo hajautustaulun, ei hajautustaulu sisällä alussa yhtäkään listaa. Ne on luotava tarvittaessa erikseen.

HashMap<String, ArrayList<String>> puhelinnumerot = new HashMap<>();

// liitetään Pekka-nimeen ensin tyhjä ArrayList
puhelinnumerot.put("Pekka", new ArrayList<>());

// ja lisätään Pekkaa vastaavalle listalle puhelinnumero
puhelinnumerot.get("Pekka").add("040-12348765");
// ja lisätään toinenkin puhelinnumero
puhelinnumerot.get("Pekka").add("09-111333");

System.out.println("Pekan numerot: " + puhelinnumerot.get("Pekka"));
Pekan numero: [040-12348765, 09-111333]

Määrittelimme muuttujan puhelinnumero tyypiksi HashMap<String, ArrayList<String>>. Tämä tarkoittaa hajautustaulua, joka käyttää avaimena merkkijonoa ja arvona merkkijonoja sisältävää listaa. Hajautustauluun lisättävät arvot ovat siis konkreettisia listoja.

// liitetään Pekka-nimeen ensin tyhjä ArrayList
puhelinnumerot.put("Pekka", new  ArrayList<>());

// ...

Vastaavalla tyylillä voi toteuttaa esimerkiksi tehtävien pistekirjanpidon. Alla olevassa esimerkissä on hahmoteltu luokkaa Tehtavakirjanpito, mikä sisältää käyttäjäkohtaisen pistekirjanpidon. Käyttäjä esitetään merkkijonona ja pisteet kokonaislukuina.

public class Tehtavakirjanpito {
    private HashMap<String, ArrayList<Integer>> tehdytTehtavat;

    public Tehtavakirjanpito() {
        this.tehdytTehtavat = new HashMap<>();
    }

    public void lisaa(String kayttaja, int tehtava) {
        // uudelle käyttäjälle on lisättävä HashMapiin tyhjä lista jos sitä
        // ei ole jo lisätty
        this.tehdytTehtavat.putIfAbsent(kayttaja, new ArrayList<>());

        // haetaan ensin käyttäjän tehtävät sisältävä lista ja tehdään siihen lisäys
        ArrayList<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja);
        tehdyt.add(tehtava);

        // edellinen olisi onnitunut myös ilman apumuuttujaa seuraavasti
        // this.tehdytTehtavat.get(kayttaja).add(tehtava);
    }

    public void tulosta() {
        for (String nimi: tehdytTehtavat.keySet()) {
            System.out.println(nimi + ": " + tehdytTehtavat.get(nimi));
        }
    }
}
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito();
kirjanpito.lisaa("Ada", 3);
kirjanpito.lisaa("Ada", 4);
kirjanpito.lisaa("Ada", 3);
kirjanpito.lisaa("Ada", 3);

kirjanpito.lisaa("Pekka", 4);
kirjanpito.lisaa("Pekka", 4);

kirjanpito.lisaa("Matti", 1);
kirjanpito.lisaa("Matti", 2);

kirjanpito.tulosta();
Matti: [1, 2]
Pekka: [4, 4]
Ada: [3, 4, 3, 3]

Tehtävänäsi on toteuttaa luokka UseanKaannoksenSanakirja, johon voidaan lisätä yksi tai useampi käännös jokaiselle sanalle. Luokan tulee toteuttaa seuraavat metodit:

  • public void lisaa(String sana, String kaannos) lisää käännöksen sanalle säilyttäen vanhat käännökset
  • public ArrayList<String> kaanna(String sana) palauttaa listan, joka sisältää sanojen käännökset. Jos sanalle ei ole yhtäkään käännöstä, metodin tulee palauttaa tyhjä lista.
  • public void poista(String sana) poistaa sanan ja sen kaikki käännökset sanakirjasta.

Käännökset kannattanee lisätä HashMap<String, ArrayList<String>>-tyyppiseen oliomuuttujaan.

Esimerkki:

UseanKaannoksenSanakirja sanakirja = new UseanKaannoksenSanakirja();
sanakirja.lisaa("kuusi", "six");
sanakirja.lisaa("kuusi", "spruce");

sanakirja.lisaa("pii", "silicon");
sanakirja.lisaa("pii", "pi");

System.out.println(sanakirja.kaanna("kuusi"));
sanakirja.poista("pii");
System.out.println(sanakirja.kaanna("pii"));
[six, spruce]
[]

Simulaatiot

Simulaatiot ovat ohjelmia, joilla pyritään kuvaamaan tai ennustamaan jonkinlaista todellista ilmiötä. Tutustutaan simulaatioihin tehtävien kautta. Tehtävissä on käytössä kaksiulotteinen maailma, jota mallinnetaan hajautustaulun avulla. Alla on kuvattu kaksiulotteisen hajautustaulun toimintaa:

// luodaan hajautustaulu
HashMap<Integer, HashMap<Integer, Integer>> taulukko = new HashMap<>();

// yllä olevassa hajautustaulussa jokainen x-koordinaatti viittaa uuteen hajautustauluun:
// näitä ei ole kuitenkaan vielä tehty, joten alustetaan kaikki arvot nolliksi

// luodaan 10 x 10 kokoinen maailma, missä kaikki arvot ovat nollia

int leveys = 10;
int korkeus = 10;

int x = 0;
while (x < leveys) {
    taulukko.putIfAbsent(x, new HashMap<>());

    int y = 0;
    while (y < korkeus) {
        taulukko.get(x).put(y, 0);
        y++;
    }

    x++;
}

// asetetaan kohtaan (3, 2) arvo 5
taulukko.get(3).put(2, 5);

// haetaan arvo kohdasta (0, 1) ja tulostetaan se
System.out.println(taulukko.get(0).get(1));

Taulukko kannattanee abstrahoida omaksi luokakseen, joka kapseloi hajautustaulun sisälleen. Alla olevassa toteutuksessa oletetaan, että tyhjissä taulukon kohdissa on arvo 0.

public class Taulukko {
    private HashMap<Integer, HashMap<Integer, Integer>> taulukko;
    private int leveys;
    private int korkeus;

    public Taulukko(int leveys, int korkeus) {
        this.leveys = leveys;
        this.korkeus = korkeus;
        this.taulukko = new HashMap<>();
    }

    public void aseta(int x, int y, int arvo) {
        if (x < 0 || x >= this.leveys || y < 0 || y >= this.korkeus) {
            return;
        }

        this.taulukko.putIfAbsent(x, new HashMap<>());
        this.taulukko.get(x).put(y, arvo);
    }

    public int hae(int x, int y) {
        if(!this.taulukko.containsKey(x)) {
            return 0;
        }

        if(!this.taulukko.get(x).containsKey(y)) {
            return 0;
        }

        return this.taulukko.get(x).get(y);
    }
}

Nyt edellä kuvattu ohjelma olisi hieman suoraviivaisempi.

Taulukko taulukko = new Taulukko(10, 10);
taulukko.aseta(3, 2, 5);
System.out.println(taulukko.hae(0, 1));

Game of Life on neljää yksinkertaista sääntöä seuraava soluautomaatti:

  1. Jos elävän solun naapureina on alle kaksi elävää solua, se kuolee alikansoituksen takia.
  2. Jos elävän solun naapureina on kaksi tai kolme elävää solua, se jää henkiin.
  3. Jos elävän solun naapureina on yli kolme elävää solua, se kuolee ylikansoituksen takia.
  4. Jos kuolleen solun naapureina on tasan kolme elävää solua, se syntyy eli muuttuu eläväksi.

Peli ei sisällä minkäänlaisia liikkumissääntöjä, mutta se silti luo tilanteita, missä erilaiset hahmot liikkuvat ruudulla. Katso pelin keksineen John Conwayn mietteitä pelistä sekä sääntöjen selitys.

Tässä tehtävässä toteutetaan oleellisilta osin Game of Life-pelin säännöt. Toteutusta varten tehtäväpohjassa on luokka GameOfLife, joka sisältää hajautustaulun avulla toteutetun kaksiulotteisen taulukon, sekä luokka GameOfLifeSovellus, jota voidaan käyttää pelin visualisointiin.

Elossa olevien naapurien lukumäärä

Täydennä luokassa GameOfLife olevaa metodia public int elossaOleviaNaapureita(Taulukko taulukko, int x, int y) siten, että se laskee annetun x, y -koordinaatin elossa olevien naapureiden lukumäärän. Naapuri on elossa mikäli sen arvo on true.

Esimerkiksi kohdassa (0, 0) oleva alkio on elossa jos kutsu taulukko.hae(x, y) palauttaa arvon true.

Naapureita ovat kaikki ne alkiot, jotka ovat kulman tai sivun kautta yhteydessä alkioon.

Esimerkiksi, jos taulukko on seuraavanlainen:

x=0,y=0: true x=1,y=0: false x=2,y=0: false
x=0,y=1: true x=1,y=1: true x=2,y=1: false
x=0,y=2: false x=1,y=2: false x=2,y=2: true

Palauttavat seuraavat kutsut seuraavat arvot:

System.out.println(gol.elossaOleviaNaapureita(taulukko, 0, 0));
System.out.println(gol.elossaOleviaNaapureita(taulukko, 1, 0));
System.out.println(gol.elossaOleviaNaapureita(taulukko, 1, 1));
System.out.println(gol.elossaOleviaNaapureita(taulukko, 2, 2));
2
3
3
1

Kehittyminen

Täydennä seuraavaksi GameOfLife-luokan metodia public void kehity() siten, että se käy yhden Game of Life -pelin askeleen.

Toteuta toiminnallisuus niin, että luot toisen taulukon, jonka koko on sama kuin alkuperäisen taulukon. Käy tämän jälkeen alkuperäistä taulukkoa läpi alkio alkiolta siten, että seuraat seuraavia sääntöjä:

  1. Jos alkuperäisen taulukon alkion arvo on true ja sillä on alle kaksi elävää naapuria, kopioon asetetaan alkion arvoksi false.
  2. Jos alkuperäisen taulukon alkion arvo on true ja sillä on kaksi tai kolme elävää naapuria, kopioon asetetaan alkion arvoksi true.
  3. Jos alkuperäisen taulukon alkion arvo on true ja sillä on yli kolme elävää naapuria, kopioon asetetaan alkion arvoksi false.
  4. Jos alkuperäisen taulukon alkion arvo on false ja sillä on tasan kolme elävää naapuria, kopioon asetetaan alkion arvoksi true.

Käytä naapureiden lukumäärän selvittämisessä edellisessä osassa tehtyä metodia. Kun olet käynyt koko taulukon läpi, vaihda kopio taulukon paikalle.

Kokeile tämän jälkeen sovelluksen toimintaa graafisen käyttöliittymän kautta. Sovelluksen pitäisi käynnistyä -- yksi mahdollinen hetkellinen tila on seuraavanlainen.

Thomas Schelling on yhdysvaltalainen taloustieteilijä, joka esitti ihmisten eriytymistä selittävän mallin. Malli perustuu ajatukselle, että vaikka ihmiset asetettasiin satunnaisesti asumaan, he muuttavat pois jos he eivät ole tyytyväisiä naapureihinsa.

Tässä tehtävässä pohditaan Schellingin mallia sekä kehitetään siihen liittyvää simulaatio-ohjelmaa.

Simulaation suoritus alkaa tilanteesta, missä ihmiset ovat asetettu satunnaisesti asumaan.

Tilanne, missä ihmiset asuvat satunnaisesti.

 

Kun simulaatio etenee, päädytään ennen pitkää tilanteeseen, missä samankaltaiset ihmiset ovat muuttaneet samankaltaisten ihmisten luo.

Ihmiset ovat muuttaneet sopivampiin paikkoihin.

 

Simulaatio-ohjelmasta puuttuu muutamia oleellisia toiminnallisuuksia: (1) kartan tyhjennys, (2) tyhjien paikkojen etsiminen, sekä (3) tyytymättömien henkilöiden tunnistaminen. Kannattaa ennen aloitusta tutustua nykyiseen tehtävän koodiin -- ohjelmassa on mukana myös visualisointiin käytettävä komponentti.

Kartan tyhjentäminen ja tyhjien paikkojen etsiminen

Simulaatiomallissa käytetään sisäkkäistä hajautustaulua kaksiulotteisen taulukon kuvaamiseen. Kohdassa (x, y) oleva arvo 0 kuvaa tyhjää paikkaa ja luvut 1-5 kuvaavat eri ryhmiä.

Toteuta ensin luokan Eriytymismalli metodiin public void tyhjenna() toiminnallisuus, joka asettaa jokaisen solun arvoksi 0.

Lisää tämän jälkeen metodiin public ArrayList<Piste> tyhjatPaikat() toiminnallisuus, joka tunnistaa tyhjät paikat (solut, joissa on arvo 0), luo jokaisesta Piste-olion, ja palauttaa ne listana. Huomaa, että käytössä olevassa hajautustaulussa ensimmäinen ulottuvuus kuvaa x-koordinaattia, ja toinen y-koordinaattia (taulukko.hae(x, y)).

Tyytymättömien hakeminen

Mallille voidaan asettaa parametri tyytyvaisyysraja. Tyytyväisyysrajalla kuvataan samaan ryhmään kuuluvien naapureiden minimimäärää, millä henkilö on tyytyväinen sijaintiinsa. Jos ruudussa (x, y) olevan henkilön naapureista on samankaltaisia yhtä paljon tai yli tyytyvaisyysrajan, on henkilö tyytyväinen. Muissa tapauksissa henkilö on tyytymätön.

Naapureista tulee tarkastella kaikkia ruudun vieressä olevia ruutuja. Alueen ulkopuolella olevat ruudut (esim. -1, 0) tulee käsitellä tyhjänä ruutuna (ei samankaltainen).

Toteuta metodi public ArrayList<Piste> haeTyytymattomat(), joka palauttaa tyytymättömät listana.

Kun metodi on toteutettu, ihaile ohjelman toimintaa :)

Vaikka karttamme on suorakulmio, voisi sen yhtä hyvin piirtää vaikkapa Helsingin muotoiseksi.

Ensimmäisen seitsemän osan lopuksi

Tämän materiaalin ensimmäiset seitsemän osaa käsittelee Helsingin yliopiston kurssia Ohjelmoinnin perusteet. Kahdeksannesta osasta lähtien käynnissä on Ohjelmoinnin jatkokurssi.

Tässä lopuksi pieni pähkinätehtävä.

Uno on korttipeli, missä jokaisella pelaajalla on kädessään kortteja. Kortteja pelataan vuorotellen siten, että jokainen pelaaja pelaa aina yhden kortin kerrallaan. Pelin voittaja on se, jonka kädestä loppuu kortit ensimmäisenä kesken.

Suurimmalla osalla korteista on väri -- Punainen, Vihreä, Sininen tai Keltainen -- ja kortin saa pelata edellisen pelaajan pelaaman kortin jälkeen jos kortilla on sama väri tai numero kuin edellisellä kortilla. Pelissä on lisäksi joukko erikoiskortteja. Osalla niistä on väri ja ne saa pelata vain jos edellisessä kortissa on sama väri. Osalla erikoiskorteista ei ole väriä. Värittömät kortit saa pelata minkä tahansa kortin jälkeen (pelaaminen tapahtuu kuitenkin aina omalla vuorolla).

  • Värilliset erikoiskortit ovat "Ohitus", "Suunnanvaihto" ja "Nosta 2". Kun pelaaja pelaa kortin "Ohitus", seuraavalta pelaajalta jää vuoro välistä. Kortti "Suunnanvaihto" kääntää pelin suuntaa (esim. myötäpäivästä vastapäivään), ja "Nosta 2" lisää seuraavalle pelaajalle kaksi korttia sekä jättää häneltä vuoron välistä.
  • Värittömät erikoiskortit ovat "Villi kortti" ja "Villi kortti + Nosta 4". Molemmat kortit antavat kortin pelaavalle pelaajalle mahdollisuuden valita seuraavaksi pelattava väri. Tämän lisäksi kortti "Villi kortti + Nosta 4" lisää seuraavalle pelaajalle 4 korttia sekä jättää häneltä vuoron välistä.

Pelin tavoitteena on saada kortit loppumaan. Kun tämä tapahtuu, ensimmäisenä korttinsa loppuun pelannut pelaaja saa pisteitä muiden pelaajien käteen jääneistä korteista. Pisteet lasketaan seuraavasti: numerokortit ovat kortissa olevan numeron arvoisia, värilliset erikoiskortit ovat 20 pisteen arvoisia, ja värittömät erikoiskortit ovat 50 pisteen arvoisia. Uusia pelejä pelataan kunnes joku pelaajista saavuttaa 500 pistettä.

Tässä tehtävässä rakennetaan Uno-peliä varten tekoäly. Ennen aloittamista, kokeile tehtäväpohjassa olevan pelin käynnistämistä ja pelaamista. Peli käynnistetään tehtäväpohjan Main-luokasta.


Tekoäly tulee toteuttaa luokkaan Tekoalypelaaja. Luokalle on valmiiksi määriteltynä rajapinta (palaamme tähän myöhemmissä osissa) ja kolme metodia.

  • Metodi public int pelaa(ArrayList<Kortti>, omatKortit, Kortti paallimmaisin, String vari, Pelitilanne tilanne) on tekoälyn ydin ja sen tehtävänä on päättää mikä kortti pelataan seuraavaksi. Metodi saa parametrina pelaajan kädessä olevat kortit (omatKortit), viimeksi pelatun kortin minkä päälle pelataan (paallimmaisin), edellisen pelaajan valitseman värin jos viimeksi pelattu kortti oli villi kortti (vari), sekä yleisen pelitilanteen (tilanne). Pelitilanne-luokasta kerrotaan enemmän tehtävänannon lopussa.
    Tehtävänäsi on toteuttaa ohjelma, joka palauttaa pelattavan kortin indeksin. Jos käytettävissä ei ole yhtäkään sopivaa korttia, metodin tulee palauttaa arvo -1, jolloin nostetaan kortti. Huomaa, että pelissä tulee pelata kortti jos se vain on mahdollista.

  • Metodia public String valitseVari(ArrayList<Kortti> omatKortit) kutsutaan kun tekoäly pelaa villin kortin eli vaihtaa pelattavaa väriä. Metodin tulee palauttaa joku seuraavista merkkijonoista: "Punainen", "Vihreä", "Sininen", "Keltainen".

  • Metodi public String nimi() kertoo tekoälysi nimen, jonka saat luonnollisesti keksiä itse. Tekoälyn nimi saattaa ilmestyä jonkinlaisiin turnauslistoihin, eli pidä nimi ns. kilttinä.

Tehtävänäsi on tutustua ohjelmaan sekä toteuttaa luokkaan Tekoalypelaaja tekoäly, joka toimii oikein, eli se pelaa aina sallitun kortin. Oikein toimiva tekoäly on kahden tehtäväpisteen arvoinen.

Kun olet saanut toteutettua oikein toimivan tekoälyn, viilaa sitä paremmaksi.

Kun tehtävän määräaika on ohi, jokaisen palauttamaa (toimivaa) tekoälyä peluutetaan muita tekoälyjä vastaan. Tulokset julkaistaan myöhemmin.


Vinkkejä viilaamiseen:

  • Koska häviötilanteessa käden pisteet menevät voittajalla, saattaa olla hyvä idea pyrkiä pelaamaan ensin kortit, joiden pistearvo on suuri.
  • Toisaalta, esimerkiksi villit kortit saattavat olla erittäin hyödyllisiä myöhemmässä vaiheessa peliä, eli niistä kannattanee pitää kiinni.
  • Tai no, ehkä villeistä korteista ei kannata pitää ikuisesti kiinni, koska ne kuitenkin ovat 50 pisteen arvoisia.
  • Kun pelaat villin kortin ja valitset värin, voi olla ihan fiksua valita väri, jota sinulla on paljon kädessä.
  • Toisaalta sitten, jos kädessäsi on punaiset 0, 1 ja 6 ja vihreä 9 sekä vihdeä nosta 2, voi olla kuitenkin ihan fiksua yrittää ensin hankkiutua eroon vihreistä korteista, sillä niiden pistearvo on hyvin suuri.
  • Pohdi tilannetta, missä pöydällä on punainen 5 ja kädessäsi on punainen 3 sekä sininen 5. Kannattaako sinun pelata punainen vai vihreä kortti? Tässä on pohdittavana sekä mahdollinen pistesaldo että syy punaisen kortin pöydällä olemiseen. Entäpä jos vastustaja on pelannut punaisen kortin koska hänellä on punaisia kortteja jäljellä?
  • Jokaista nollakorttia löytyy pelistä vain yksi, mutta jokaista numerokorttia on kaksi. Nollan pelaaminen ei vaikuta käden pisteisiin, mutta sen pelaaminen saattaa johtaa tilanteeseen, missä on epätodennäköisempää että jollain muulla on sama numero. Tällöin todennäköisyys värin vaihdolle saattaa olla pienempi..
  • Kaikkiin näihin vinkkeihin varmaankin vaikuttaa myös kädessä olevien korttien määrä. Pienellä määrällä kortteja voi olla ehkäpä hyvä ottaa riskejä ja yrittää voittaa peli. Toisaalta sitten, jos kädessä on paljon kortteja, kannattaa ehkä unohtaa kyseisen kierroksen voitto ja pyrkiä minimoimaan vastustajalle menevien pisteiden määrä.
  • Ainiin, muutkin osallistujat lukevat näitä vinkkejä.. Löytyisiköhän joillekin edeltäville vinkeille jonkinlainen käänteisstrategia?

Pelitilanne-luokka sisältää havaintoja tähän asti kuluneesta pelistä sekä lisätietoa kehittyneemmille tekoälyille. Se tarjoaa seuraavat metodit:

  • Metodi public String getSuunta() palauttaa pelin tämänhetkisen suunnan. Suunta on joko "Myötäpäivään" eli eteenpäin tai "Vastapäivään" eli taaksepäin.
  • Metodi public int getOmaIndeksi() palauttaa oman tekoälyn indeksin. Indeksiä käytetään seuraavissa metodeissa.
  • Metodi public HashMap<Integer, Integer> getPelaajienPisteet() kertoo tämänhetkisen pistetilanteen kaikille pelaajille.
  • Metodi public HashMap<Integer, Integer> getPelaajienKorttienLukumaarat() kertoo pelaajien tämänhetkisen korttien lukumäärän.
  • Metodi public HashMap<Integer, String> getPelaajienViimeksiPelaamatVarit() kertoo pelaajien viimeksi pelaamat värit. Alkiossa on arvo null jos pelaaja ei ole vielä pelannut korttia.

Tehtävän alkuperäinen idea: Stephen Davies, UMW

Sisällysluettelo