Tehtävät
Seitsemännen osan tavoitteet

Osaa pilkkoa ohjelman useampaan osaan, missä yhden vastuulla on (tekstimuotoinen) käyttöliittymä, toinen vastuulla sovelluslogiikka. Ryhmittelee arvoja hajautustaulun avulla ja osaa käyttää listaa hajautustaulun arvona. Osaa hyödyntää Javan valmiita kirjastoja satunnaisten lukujen luomiseen. Osaa luoda yksinkertaisia simulaatioita.

Kurssin väli/loppukysely

Kurssiin kuuluu kyselyitä, joiden tavoitteena on parantaa kurssia. Vastaa kyselyyn osoitteessa https://elomake.helsinki.fi/lomakkeet/83363/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.

Tässä tehtävässä kerrataan olio-ohjelmoinnin perusteita sekä listojen käsittelyä virran avulla.

Kumpulan tiedekirjasto tarvitsee uuden järjestelmän kirjojen hallintaan. Tässä tehtävässä hahmotellaan ongelma-alueen tietosisältöä ja toteutetaan prototyyppi, joka mahdollistaa kirjan haun nimen, julkaisijan tai julkaisuvuoden perusteella.

Rakennetaan järjestelmä osista, ensin toteutetaan oleelliset luokat eli Kirja ja Kirjasto. Luokka Kirja sisältää kirjaan liittyvät tiedot, luokka Kirjasto tarjoaa erilaisia hakutoiminnallisuuksia kirjoihin liittyen.

Kirja

Luodaan ensiksi luokka Kirja. Kirjalla on oliomuuttujina nimeke, eli kirjan nimi, julkaisija, eli kirjan julkaisija, ja julkaisuvuosi eli vuosi jolloin kirja on julkaistu. Kaksi ensimmäistä muuttujaa on merkkijonotyyppisiä, viimeisin on kokonaisluku. Oletamme tässä että kirjalla on aina vain yksi kirjoittaja.

Toteuta luokka Kirja. Kirjalla tulee olla myös konstruktori public Kirja(String niemeke, String julkaisija, int julkaisuvuosi) sekä metodit public String nimeke(), public String julkaisija(), public int julkaisuvuosi() ja public String toString(). Arvannet mitä metodien tulee tehdä, alla esimerkki.

Testaa luokan toimintaa:

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
System.out.println(cheese.nimeke());
System.out.println(cheese.julkaisija());
System.out.println(cheese.julkaisuvuosi());

System.out.println(cheese);
Cheese Problems Solved
Woodhead Publishing
2007
Cheese Problems Solved, Woodhead Publishing, 2007

Kirjasto

Kirjaston tehtävä on antaa käyttäjälle mahdollisuus kirjojen lisäämiseen ja niiden hakemiseen. Luo luokka Kirjasto, jolla on konstruktori public Kirjasto() ja metodit public void lisaaKirja(Kirja uusiKirja) ja public void tulostaKirjat()

Kirjasto kirjasto = new Kirjasto();

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
kirjasto.lisaaKirja(cheese);

Kirja nhl = new Kirja("NHL Hockey", "Stanley Kupp", 1952);
kirjasto.lisaaKirja(nhl);

kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

kirjasto.tulostaKirjat();
Cheese Problems Solved, Woodhead Publishing, 2007
NHL Hockey, Stanley Kupp, 1952
Battle Axes, Tom A. Hawk, 1851

Hakutoiminnallisuus

Kirjastosta tulee pystyä etsimään kirjoja nimekkeiden ja julkaisijoiden perusteella. Lisää kirjastolle metodit public ArrayList<Kirja> haeKirjaNimekkeella(String nimeke), public ArrayList<Kirja> haeKirjaJulkaisijalla(String julkaisija) ja public ArrayList<Kirja> haeKirjaJulkaisuvuodella(int julkaisuvuosi). Metodit palauttavat listan kirjoista, joissa on haluttu nimeke, julkaisija tai julkaisuvuosi.

Voit halutessasi hyödyntää seuraavaa runkoa metodin tekemiseen.

public class Kirjasto {
    // ...

    public ArrayList<Kirja> haeKirjaNimekkeella(String nimeke) {
        ArrayList<Kirja> loydetyt = new ArrayList<>();

        // käy läpi kaikki kirjat ja lisää ne joilla haetun kaltainen nimeke listalle loydetyt

        return loydetyt;
    }
}

Huom! Kun haet teet hakua merkkijonon avulla, älä tee tarkkaa hakua (metodi equals) vaan käytä String-luokan metodia contains. Huomaat todennäköisesti myös että sinulla on ns. copy-paste -koodia Kirjasto-luokan koodissa. Keksitkö tavan päästä siitä eroon?

Kirjasto kirjasto = new Kirjasto();

kirjasto.lisaaKirja(new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007));
kirjasto.lisaaKirja(new Kirja("The Stinky Cheese Man and Other Fairly Stupid Tales", "Penguin Group", 1992));
kirjasto.lisaaKirja(new Kirja("NHL Hockey", "Stanley Kupp", 1952));
kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

kirjasto.haeKirjaNimekkeella("Cheese").forEach(k -> System.out.println(k));

System.out.println("---");
kirjasto.haeKirjaJulkaisijalla("Pong Group").forEach(k -> System.out.println(k));

System.out.println("---");
kirjasto.haeKirjaJulkaisuvuodella(1851).forEach(k -> System.out.println(k));
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
---
Battle Axes, Tom A. Hawk, 1851

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. Vastaavasti käyttöliittymä on 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.

Hahmotellaan Kivi-paper-sakset -peliä sekä siihen liittyvää "tekoälyä". Peli kysyy käyttäjältä siirtoa, kysyy sen jälkeen tietokoneen valintaa ja lopulta kertoo tuloksen. Pelin lopettaminen onnistuu 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. Ei kovin älykäs siis.

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 ohjelmoinnin jatkokurssilla 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 {
    // oliomuuttujat
    private Scanner lukija;
    private Tekoaly tekoaly;
    private Saannot saannot;
    private Pistetilanne pistetilanne;

    
    // konstruktori
  
    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;
    }
}

Tehtäväpohjassa on edellisessä esimerkissä kuvattu kivi, paperi, sakset -peli. Pelin tekoäly on kuitenkin harmittavan heikko. Tässä tehtävässä kehität pelin tekoälyä.

Eräs hyvä tekoälystrategia perustuu tietoon siitä, että ihmisten toiminta on helposti ennakoitavissa.

Laajenna Tekoaly-luokkaa siten, että se pitää kirjaa vastustajan tekemistä siirroista listassa. Metodi tallennaVastustajanSiirto lisää vastustajan siirron tekoälyn muistiin.

Muuta Tekoaly-luokan annaSiirto-metodia siten, että se käyttää muistissa olevaa listaa seuraavan siirron tekemiseen. Tässä erittäin hyvä strategia on katsoa vastustajan viimeisintä kahta siirtoa ja etsiä vastustajan pelihistoriasta niitä seuraava todennäköisin siirto.

Tarkastellaan esimerkiksi tilannetta, missä tekoälyn muistissa on siirrot [p, p, k, k, s, p, k, k, s, p, k, k]. Vastustajan kaksi viimeisintä siirtoa ovat kivi ja kivi. Muistin perusteella voidaan laskea "todennäköisyydet" sille, että seuraava siirto on kivi, paperi tai sakset.

  • kivi, kivi, kivi: esiintyy muistissa 0 kertaa.
  • kivi, kivi, paperi: esiintyy muisissa 0 kertaa.
  • kivi, kivi, sakset: esiintyy muistissa 2 kertaa.

Muistin perusteella arvaamme, että pelaajan seuraava siirto on "sakset". Tekoälyn kannattaa siis pelata kivi.

Tämä tehtävä vastaa kahta yksiosaista tehtävää. Tehtävään ei ole testejä -- testaa tekoälyn toimintaa pelaamalla sitä vastaan itse.

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.

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() {
        this.tehdytTehtavat.entrySet().stream().forEach(entry -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}
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]
[]

Satunnaisuus ohjelmissa

Satunnaisuutta tarvitaan esimerkiksi salausalgoritmeissa, koneoppimisessa sekä tietokonepelien ennustettavuuden vähentämisessä. Satunnaisuutta mallinnetaan käytännössä satunnaislukujen avulla, joiden luomiseen Java valmiin Random-luokan. Random-luokasta voi tehdä olion jota voi käyttää seuraavalla tavalla.

import java.util.Random;

public class Arvontaa {
    public static void main(String[] args) {
        Random arpoja = new Random(); // luodaan arpoja apuväline
        int i = 0;

        while (i < 10) {
            // Arvotaan ja tulostetaan jokaisella kierroksella satunnainen luku
            System.out.println(arpoja.nextInt(10));
            i++;
        }
    }
}

Yllä olevassa koodissa luodaan ensin Random-luokasta olio käyttäen avainsanaa new -- samoin kuin muitakin olioita luodessa. Random-olio tarjoaa metodin nextInt, jolle annetaan parametrina kokonaisluku. Metodi palauttaa satunnaisen kokonaisluvun väliltä 0..(annettu kokonaisluku - 1).

Ohjelman tulostus voisi olla vaikka seuraavanlainen:

2
2
4
3
4
5
6
0
7
8

Satunnaisia kokonaislukuja voidaan käyttää esimerkiksi nopanheittojen mallintamiseen.

Tehtäväpohjassa on luokka Noppa, jonka runko on seuraava:

import java.util.Random;

public class Noppa {
    private Random random;
    private int tahkojenMaara;

    public Noppa(int tahkojenMaara) {
        this.random = new Random();
        // Alusta oliomuuttuja tahkojenMaara tässä 
    }

    public int heita() {
        // arvo täällä luku jonka tulee olla yhdestä tahkojen määrään
        // ([1-tahkojenMaara]) ja palauta se
    }
}

Muokkaa luokkaa siten, että sen konstruktoriNoppa(int tahkojenMaara) luo uuden noppa-olion annetulla nopan tahkojen (eri oman numeronsa sisältämien "puolien") määrällä. Muokkaa myös metodia heita siten, että se antaa satunnaisen nopanheiton tuloksen, jonka arvon tulee olla väliltä 1...tahkojen määrä (vinkki: plus!).

Seuraavassa noppaa testaava pääohjelma:

public class Ohjelma {
    public static void main(String[] args) {
        Noppa noppa = new Noppa(6);

        int i = 0;
        while (i < 10) {
            System.out.println(noppa.heita());
            i++;
        }
    }
}

Tulostus voisi olla esimerkiksi seuraava:

1
6
3
5
3
3
2
2
6
1

Random-luokasta tehdyn olion kautta päästään käsiksi myös satunnaisiin liukulukuihin, joita käytetään muunmuassa todennäköisyyslaskennan yhteydessä; tietokoneilla todennäköisyyksiä simuloidaan yleensä väliltä [0..1] olevilla luvuilla.

Random-oliolta satunnaisia liukulukuja saa metodilla nextDouble. Tarkastellaan seuraavia säämahdollisuuksia:

  • Sataa räntää todennäköisyydellä 0.1 (10%)
  • Sataa lunta todennäköisyydellä 0.3 (30%)
  • Aurinko paistaa todennäköisyydellä 0.6 (60%)

Luodaan edellä olevista arvioista sääennustaja.

import java.util.ArrayList;
import java.util.Random;

public class SaaEnnustaja {
    private Random random;

    public SaaEnnustaja() {
        this.random = new Random();
    }

    public String ennustaSaa() {
        double todennakoisyys = this.random.nextDouble();

        if (todennakoisyys <= 0.1) {
            return "Sataa räntää";
        } else if (todennakoisyys <= 0.4) { // 0.1 + 0.3
            return "Sataa lunta";
        } else { // loput, 1.0 - 0.4 = 0.6
            return "Aurinko paistaa";
        }
    }

    public int ennustaLampotila() {
        return (int) (4 * this.random.nextGaussian() - 3);
    }
}

Metodi ennustaLampotila on monella tapaa mielenkiintoinen. Metodin sisällä tehtävä kutsu this.random.nextGaussian() on tavallinen metodikutsu, jonka kaltaisia olemme nähneet aikaisemminkin. Kiinnostavaa tässä Random-luokan ilmentymän tarjoamassa metodissa on se, että metodin palauttama luku on normaalijakautunut (jos et koe mielenkiintoa satunnaisuuden eri lajeihin se ei haittaa!).

public int ennustaLampotila() {
    return (int) (4 * this.random.nextGaussian() - 3);
}

Edellisessä esimerkissä käytetään eksplisiittistä tyyppimuunnosta liukulukujen muuntamiseen kokonaisluvuiksi (int). Vastaavalla menetelmällä voidaan muuttaa myös kokonaislukuja liukuluvuiksi kirjoittamalla (double) kokonaisluku

Luodaan vielä pääohjelma josta luokkaa SaaEnnustaja käytetään.

public class Ohjelma {

    public static void main(String[] args) {
        SaaEnnustaja ennustaja = new SaaEnnustaja();

        // tallennetaan päivät listalle
        ArrayList<String> paivat = new ArrayList<>();
        paivat.add("Ma");
        paivat.add("Ti");
        paivat.add("Ke");
        paivat.add("To");
        paivat.add("Pe");
        paivat.add("La");
        paivat.add("Su");

        System.out.println("Seuraavan viikon sääennuste:");
        paivat.stream().forEach(paiva -> {
            String saaEnnuste = ennustaja.ennustaSaa();
            int lampotilaEnnuste = ennustaja.ennustaLampotila();

            System.out.println(paiva + ": " + saaEnnuste + " " + lampotilaEnnuste + " astetta.");
        });
    }
}

Ohjelman tulostus voisi olla esimerkiksi seuraavanlainen:

Seuraavan viikon sääennuste:
Ma: Sataa lunta 1 astetta.
Ti: Sataa lunta 1 astetta.
Ke: Aurinko paistaa -2 astetta.
To: Aurinko paistaa 0 astetta.
Pe: Sataa lunta -3 astetta.
La: Sataa lunta -3 astetta.
Su: Aurinko paistaa -5 astetta

Tehtävänäsi on täydentää luokkaa Lottorivi, joka arpoo viikon lottonumerot. Lottonumerot ovat väliltä 1–40 ja niitä arvotaan 7. Lottorivi koostuu siis seitsemästä eri numerosta väliltä 1–40. Luokassa on seuraavat toiminnot:

  • konstruktori Lottorivi luo uuden Lottorivi-olion joka sisältää uudet, arvotut numerot
  • metodi numerot palauttaa tämän lottorivin lottonumerot
  • metodi sisaltaaNumeron kertoo onko arvotuissa numeroissa annettu numero
  • metodi arvoNumerot arpoo riville uudet numerot

Luokan runko on seuraava:

import java.util.ArrayList;
import java.util.Random;

public class LottoRivi {
    private ArrayList<Integer> numerot;

    public LottoRivi() {
        // Arvo numerot heti LottoRivin luomisen yhteydessä
        this.arvoNumerot();
    }

    public ArrayList<Integer> numerot() {
        return this.numerot;
    }

    public boolean sisaltaaNumeron(int numero) {
        // Testaa tässä onko numero jo arvottujen numeroiden joukossa
    }

    public void arvoNumerot() {
        // Alustetaan lista numeroille
        this.numerot = new ArrayList<>();
        // Kirjoita numeroiden arvonta tänne käyttämällä metodia sisaltaaNumeron()
    }
}

Tehtäväpohjan mukana tulee seuraava pääohjelma:

import java.util.ArrayList;

public class Ohjelma {
    public static void main(String[] args) {
        Lottorivi rivi = new Lottorivi();
        ArrayList<Integer> lottonumerot = rivi.numerot();

        System.out.println("Lottonumerot:");
        lottonumerot.stream().forEach(numero -> {
            System.out.print(numero + " ");
        });

        System.out.println("");
    }
}

Ohjelman mahdollisia tulostuksia ovat seuraavat:

Lottonumerot:
3 5 10 14 15 27 37
Lottonumerot:
2 9 11 18 23 32 34

Huom! Sama numero saa esiintyä lottorivissä vain kerran. Lottorivin numeroiden ei tarvitse olla järjestyksessä.

Lukujen satunnaisuudesta

Tietokoneiden toiminta on ennustettavissa sillä ne suorittavat niille annettuja komentoja orjallisesti. Ovatko siis tietokoneen luomat luvut todellisuudessa satunnaisia?

Tietokoneohjelmissa käytetyt satunnaisluvut ovat tyypillisesti pseudosatunnaislukuja, eli ne vaikuttavat satunnaisluvuilta, mutta seuraavat todellisuudessa jonkinlaista algoritmisesti luotua toistuvaa lukusarjaa. Suurimmalle osalle tietokoneohjelmista pseudosatunnaisluvut ovat riittävän hyviä -- esimerkiksi youtube-videoiden satunnaisessa toistossa normaali käyttäjä tuskin huomaa eroa. Toisaalta, jos satunnaislukuja käytetään tieteelliseen laskentaan, heikosti toimivat pseudosatunnaislukuja luovat algoritmit saattavat jopa johtaa tulosten kyseenalaistamiseen. Eräs esimerkki tällaisesta on hetken 1960-luvulla käytössä ollut IBM:n RANDU.

Kaikki tietokoneohjelmien satunnaisuus ei kuitenkaan ole pseudosatunnaisuutta. Vahvempaan satunnaisuuteen pyrkivät ohjelmat hyödyntävät muunmuassa jonkinlaisia tosielämän satunnaiseksi ajateltuja ilmiöitä satunnaislukujen luomiseen. Tällaisia ilmiöitä ovat esimerkiksi avaruussäteily tai vaikkapa laavalamppujen toiminta.

Lisää aiheesta osoitteessa https://www.random.org/randomness/.

Simulaatiot

Simulaatiot ovat ohjelmia, joilla pyritään kuvaamaan tai ennustamaan jonkinlaista todellista ilmiötä. Tutustutaan simulaatioihin kolmen tehtävän kautta. Kaikissa tehtävissä on käytössä kaksiulotteinen maailma, jota mallinnetaan hajautustaulun avulla. Alla on kuvattu kaksiulotteisen hajautustaulu 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(x).put(y, 5);

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

Oletetaan, että henkilö aloittaa kävelynsä pisteestä (0, 0). Tietämättä maailmasta enempää, onko mahdollista, että henkilö voi kulkea pisteeseen (20, 20)?

Kursseilla tietorakenteet ja algoritmit sekä johdatus tekoälyyn tarkastellaan muunmuassa reitinhakualgoritmeja, joiden avulla voidaan selvittää nopeimpia reittejä annettujen pisteiden välillä. Tässä tarkastelemme ongelmaa hieman erilaisen menetelmän avulla ja tutustumme satunnaiskävelyyn (satunnaiskulku, random walk).

Satunnaiskävelyssä todennäköisyydet liikkeelle ovat näennäisesti satunnaisia. Toteutettavassa tapauksessamme henkilö liikkuu seuraavasti: 20% todennäköisyydellä hän pysyy paikallaan, 20% todennäköisyydellä hän liikkuu oikealle, 20% todennäköisyydellä hän liikkuu vasemmalle, 20% todennäköisyydellä hän liikkuu ylös, ja 20% todennäköisyydellä hän liikkuu alas.

Jos algoritmi ei ole saavuttanut pistettä (20, 20) hyvin ison askelmäärän jälkeen, voimme ehdottaa, että pisteeseen (20, 20) ei ole pääsyä.

Tehtäväpohjassa on annettu satunnaiskävelyn piirtämistä varten luotu ohjelma, missä kaksiulotteista maailmaa maillinnetaan kahden sisäkkäisen HashMap-tietorakenteen avulla seuraavastiHashMap<Integer, HashMap<Integer, Double>> taulukko. Koordinaatissa 0, 0 olevaan arvoon pääsee käsiksi kutsumalla taulukko.get(20).get(20).

Tehtäväpohjassa on lisäksi mallinnettu tilannetta, missä jokainen askel jättää hiljalleen haihtuvan jäljen.

Tehtävänäsi on tutustua ohjelmaan ja lisätä siihen toiminnallisuus, missä jokainen askel on satunnainen. Henkilön askelten tulee tapahtua seuraavasti: 20% todennäköisyydellä henkilö pysyy paikallaan, 20% todennäköisyydellä henkilö liikkuu oikealle, 20% todennäköisyydellä henkilö liikkuu vasemmalle, 20% todennäköisyydellä henkilö liikkuu ylös, ja 20% todennäköisyydellä henkilö liikkuu alas.

Alla on kuvakaappaus eräästä satunnaiskävelystä.

Esimerkki (lyhyestä) satunnaiskävelystä)

Vaikka Random walk -menetelmä tuntuu hyvin yksinkertaiselta, sillä on monia sovelluksia. Sitä käytetään muunmuassa molekyylien satunnaisen liikkeen mallintamisessa, osakkeiden hintojen muutosten mallintamisessa, ja geenien satunnaisissa muutoksissa.

Tehtävään ei ole testejä -- palauta se kun se toimii halutusti.

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(HashMap<Integer, HashMap<Integer, Boolean>> taulukko, int x, int y) siten, että se laskee annetun x, y -koordinaatin elossa olevien naapureiden lukumäärän. Naapuri on elossa jos sen arvo on 1.

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

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

Huomaa, että metodin tulee varoa taulukon ulkopuolelle menemistä. Indeksissä -1 ei esimerkiksi voi olla ketään. Vastaavasti leveyden tai korkeuden yli ei voi mennä (esim. taulukko.get(taulukko.size()).get(0) tai taulukko.get(0).get(taulukko.size())).

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 1 ja sillä on alle kaksi elävää naapuria, kopioon asetetaan alkion arvoksi 0.
  2. Jos alkuperäisen taulukon alkion arvo on 1 ja sillä on kaksi tai kolme elävää naapuria, kopioon asetetaan alkion arvoksi 1.
  3. Jos alkuperäisen taulukon alkion arvo on 1 ja sillä on yli kolme elävää naapuria, kopioon asetetaan alkion arvoksi 0.
  4. Jos alkuperäisen taulukon alkion arvo on 0 ja sillä on tasan kolme elävää naapuria, kopioon asetetaan alkion arvoksi 1.

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 asetaKaikkiTyhjiksi() 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.get(x).get(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.

Kurssin lopuksi

Kurssin lopuksi vielä pieni pähkinätehtävä, jonka parhaiten pärjääviä toteutuksia tarkastellaan ohjelmoinnin jatkokurssin ensimmäisellä luennolla.

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 kaikkia muita tekoälyjä vastaan useampaan otteeseen. Parhaille tekoälyille on tiedossa myös palkintoja.


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ä.
  • Toiisaalta 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