Pelilauta.java

package datastructureproject.luokat;

import chess.model.Side;
import datastructureproject.luokat.apulaiset.Matikka;
import datastructureproject.luokat.nappulat.Kuningas;
import datastructureproject.luokat.nappulat.Kuningatar;
import datastructureproject.luokat.nappulat.Lahetti;
import datastructureproject.luokat.nappulat.Nappula;
import datastructureproject.luokat.nappulat.Ratsu;
import datastructureproject.luokat.nappulat.Sotilas;
import datastructureproject.luokat.nappulat.Torni;
import datastructureproject.luokat.tietorakenteet.*;

public class Pelilauta {
    /** 
     * Laudan alkutilan esitys. Versaalit kuvaavat mustan pelaajan nappuloita.
     * r = torni, n = ratsu, b = lähetti, q = kuningatar, k = kuningas, p = sotilas
     */
    public static final char[][] ALKUTILANNE = new char[][] {
        {'r', 'n', 'b', 'q', 'k', 'b', 'n', 'r' }, 
        {'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p' }, 
        {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, 
        {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, 
        {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, 
        {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }, 
        {'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P' }, 
        {'R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R' }
    };

    /**
     * Kuvaa shakkilaudan tilannetta
     */
    public final Nappula[][] lauta;
    private Ruutu valkoinenKuningas;
    private Ruutu mustaKuningas;


    private Lista<Lista<Tupla<Ruutu, Nappula>>> historia = new Lista<>();

    /**
     * Luo uuden pelilaudan shakin aloitustilanteelle
     */
    public Pelilauta() {
        lauta = new Nappula[ALKUTILANNE.length][ALKUTILANNE[0].length];
        alusta(ALKUTILANNE);
    }

    /**
     * Luo uuden pelilaudan testitilanteen tarkastelua varten
     * @param templaatti pelin alkutilanne tai keskeneräisen pelin tilanne
     */
    public Pelilauta(char[][] templaatti) {
        lauta = new Nappula[templaatti.length][templaatti[0].length];
        alusta(templaatti);
    }

    private void talletaKuningasMuistiin (Nappula n, Ruutu kohde) {
        //Tallennetaan kuninkaan sijainti muistiin
        if (n instanceof Kuningas) {
            if (n.getPuoli() == Side.WHITE) { 
                valkoinenKuningas = kohde.kopioi(); 
            } else {
                mustaKuningas = kohde.kopioi(); 
            }
        }
    }

    /**
     * Toteuttaa siirron tällä laudalla
     * 
     * @param siirto
     */
    public void siirto(Siirto siirto) {
        Nappula siirrettava = getNappula(siirto.getAlku());
        Lista<Tupla<Ruutu, Nappula>> historiaSiirrot = new Lista<>();

        //talleta liikkeen alkuruutu
        historiaSiirrot.add(new Tupla<>(siirto.getAlku().kopioi(), siirrettava.kopioi()));
        //talleta liikkeen kohderuutu
        if (getNappula(siirto.getKohde()) != null) {
            historiaSiirrot.add(new Tupla<>(siirto.getKohde().kopioi(), getNappula(siirto.getKohde()).kopioi()));
        } else {
            historiaSiirrot.add(new Tupla<>(siirto.getKohde().kopioi(), (Nappula) null));
        }

        nollaaRuutu(siirto.getAlku());

        //Tallennetaan kuninkaan sijainti muistiin
        if (siirrettava instanceof Kuningas) {
            talletaKuningasMuistiin(siirrettava, siirto.getKohde());
        }

        //Erikoissiirrot:
        //https://fi.wikipedia.org/wiki/Tornitus
        //Tornitus
        int siirronPituusX = Matikka.itseisarvo(siirto.getAlku().getX() - siirto.getKohde().getX());
        int siirronPituusY = Matikka.itseisarvo(siirto.getAlku().getY() - siirto.getKohde().getY());

        if (siirrettava instanceof Kuningas && siirronPituusX > 1) {
            boolean kohdeIsompi  = (siirto.getKohde().getX() - siirto.getAlku().getX() > 0);
            int torniX = kohdeIsompi ? getKoko() - 1 : 0;
            int torniUusiX = 1 + (kohdeIsompi ? siirto.getAlku().getX() : siirto.getKohde().getX());

            Nappula torni = lauta[siirto.getAlku().getY()][torniX];
            if (!(torni instanceof Torni)) {
                throw new Error("Joku yritti tornittaa ilman tornia");
            }
                
            // Tornituksen sääntöjen mukaan torni ei saa uudessa ruudussa tulla uhatuksi 
            // (koska kuningas olisi tavallisesti liikkunut siihen 
            // ja shakin erikoisliikkeiden erikoissäännöt ovat hyvin erikoisia)
            // Oletetaan kuitenkin että vihollinen tekee vain sallittuja 
            // liikkeitä, eikä itse tehdä tornituksia
            // Jos tornitukset sisälletään omaan liikehdintään, tulee silloin tarkastaa ettei torni tule uhatuksi

            //Talletetaan tornin alkuruutu historiaan
            historiaSiirrot.add(new Tupla<>(torni.getRuutu().kopioi(), torni.kopioi()));

            //Siirretään torni 
            lauta[siirto.getAlku().getY()][torniX] = null;
            torni.setRuutu(torniUusiX, siirto.getAlku().getY());
            lauta[siirto.getAlku().getY()][torniUusiX] = torni;

            //Talletetaan tornin kohderuutu historiaan
            historiaSiirrot.add(new Tupla<>(torni.getRuutu().kopioi(), (Nappula) null));
        }

        //https://fi.wikipedia.org/wiki/Ohestaly%C3%B6nti
        //Prosessoi Ohestalyönti liike, olettaen ettei vastustaja tee laittomia liikkeitä
        if (siirronPituusY >= 1
                && siirronPituusX >= 1
                && siirrettava instanceof Sotilas
                && getNappula(siirto.getKohde()) == null) {
            int uusiY = siirto.getKohde().getEteenpainY(siirrettava.getPuoli(), -1);
            Ruutu ohestaLyoty = new Ruutu(siirto.getKohde().getX(), uusiY);

            //Talletetaan ohestalyöty nappula historiaan
            historiaSiirrot.add(new Tupla<>(ohestaLyoty.kopioi(), getNappula(ohestaLyoty).kopioi()));

            lauta[ohestaLyoty.getY()][ohestaLyoty.getX()] = null;
        }

        
        //https://fi.wikipedia.org/wiki/Sotilas_(shakki)
        //Prosessoi sotilaan ylennys
        if (siirrettava instanceof Sotilas && siirto.onYlennys()) {

            Side puoli = siirrettava.getPuoli();
            Ruutu ruutu = siirto.getKohde().kopioi();
            if (siirto.getYlennys() == 'q') {
                siirrettava = new Kuningatar(puoli, ruutu);
            } else if (siirto.getYlennys() == 'n') {
                siirrettava = new Ratsu(puoli, ruutu);
            } else if (siirto.getYlennys() == 'b') {
                siirrettava = new Lahetti(puoli, ruutu);
            } else if (siirto.getYlennys() == 'r') {
                siirrettava = new Torni(puoli, ruutu);
            } else {
                throw new Error("Joku yritti ylentää sotilaan sallimattomaksi nappulaksi (" 
                    + siirto.getYlennys() + ")");
            }
            
        } 

        //Sijoittaa nappulan laudalle ja päivittää nappulan oman sijainnin tiedon
        sijoitaNappula(siirrettava, siirto.getKohde().kopioi());

        //Lisätään tämän siirron vaikutukset historialistaan
        historia.add(historiaSiirrot);
    }

    /**
     * Peruutetaan viimeksi toteutettu siirto (toimii vastakohtana siirto(Siirto s) funktiolle)
     */
    public void peruutaSiirto() {
        Lista<Tupla<Ruutu, Nappula>> historiaLista = historia.pop();

        for (int i = 0; i < historiaLista.size(); i++) {
            Tupla<Ruutu, Nappula> hs = historiaLista.get(i);

            Ruutu r = hs.getEka();
            Nappula n = hs.getToka();
            if (n != null) {
                n.setRuutu(r);
            }
            lauta[r.getY()][r.getX()] = n;

            if (n instanceof Kuningas) {
                talletaKuningasMuistiin(n, r);
            }
        }
    }

    /**
     * Palauttaa pelaajan Kuningas-nappulan
     * 
     * @param puoli kumman pelaajan kuningas etsitään
     * @return tämän pelaajan kuningas
     */
    public Kuningas etsiKuningas(Side puoli) {
        Nappula kuningas = puoli == Side.WHITE ? getNappula(valkoinenKuningas) : getNappula(mustaKuningas);
        return (kuningas instanceof Kuningas) ? (Kuningas) kuningas : null;
    }

    /**
     * 
     * @param x koordinaatti laudalla
     * @param y koordinaatti laudalla
     * @return koordinaateissa olevan nappulan
     */
    public Nappula getNappula(int x, int y) {
        return lauta[y][x];
    }

    /**
     * 
     * @param ruutu ruutu jossa oleva nappula halutaan loytaa
     * @return nappulan joka on ruudussa
     */
    public Nappula getNappula(Ruutu ruutu) {
        return lauta[ruutu.getY()][ruutu.getX()];
    }

    /**
     * Poistaa ruudussa olevan nappulan
     * @param ruutu ruutu, jossa oleva nappula halutaan poistaa
     */
    public void nollaaRuutu(Ruutu ruutu) {
        lauta[ruutu.getY()][ruutu.getX()] = null;
    }

    /**
     * 
     * @param nappula Nappula, joka sijoitetaan (takaisin pelilaudalle)
     * @param ruutu Ruutu johon nappula laitetaan
     */
    private void sijoitaNappula(Nappula nappula, Ruutu ruutu) {
        lauta[ruutu.getY()][ruutu.getX()] = nappula;
        if (nappula != null) {
            nappula.setRuutu(ruutu.getX(), ruutu.getY());
        }
    }

    /**
     * Generoi tietyn puolen pelaajan kaikkien nappuloiden liikkeet.
     * 
     * @param puoli kumman pelaajan vuoro on. 
     * @return listan siirroista jotka pelaaja voi tehdä. 
     */
    public Lista<Siirto> generoiSiirrot(Side puoli) {
        Lista<Siirto> siirrot = new Lista<>();
        
        for (int y = 0; y < getKoko(); y++) {
            for (int x = 0; x < getKoko(); x++) {
                Nappula n = lauta[y][x];
                if (n != null && n.getPuoli() == puoli) {
                    Lista<Siirto> tSiirrot = n.generoiSiirrot(this);
                    siirrot.addAll(tSiirrot);
                }
            }
        }

        return siirrot;
    } 

    /**
     * Alustaa pelilaudan templaatin mukaiseen alkuasentoon
     */
    private void alusta(char[][] templaatti) {
        for (int y = 0; y < templaatti.length; y++) {
            for (int x = 0; x < templaatti[0].length; x++) {
                switch (templaatti[y][x]) {
                    case 'r':
                        lauta[y][x] = new Torni(Side.WHITE, new Ruutu(x, y));
                        break;
                    case 'n':
                        lauta[y][x] = new Ratsu(Side.WHITE, new Ruutu(x, y));
                        break;
                    case 'b':
                        lauta[y][x] = new Lahetti(Side.WHITE, new Ruutu(x, y));
                        break;
                    case 'q':
                        lauta[y][x] = new Kuningatar(Side.WHITE, new Ruutu(x, y));
                        break;
                    case 'k':
                        valkoinenKuningas = new Ruutu(x, y);
                        lauta[y][x] = new Kuningas(Side.WHITE, new Ruutu(x, y));
                        break;
                    case 'p':
                        lauta[y][x] = new Sotilas(Side.WHITE, new Ruutu(x, y));
                        break;

                    case 'R':
                        lauta[y][x] = new Torni(Side.BLACK, new Ruutu(x, y));
                        break;
                    case 'N':
                        lauta[y][x] = new Ratsu(Side.BLACK, new Ruutu(x, y));
                        break;
                    case 'B':
                        lauta[y][x] = new Lahetti(Side.BLACK, new Ruutu(x, y));
                        break;
                    case 'Q':
                        lauta[y][x] = new Kuningatar(Side.BLACK, new Ruutu(x, y));
                        break;
                    case 'K':
                        mustaKuningas = new Ruutu(x, y);
                        lauta[y][x] = new Kuningas(Side.BLACK, new Ruutu(x, y));
                        break;
                    case 'P':
                        lauta[y][x] = new Sotilas(Side.BLACK, new Ruutu(x, y));
                        break;
                    default:
                        break;
                }

            }
        }
    }


    /**
     * Palauttaa laudan leveyden / pituuden
     * @return laudan koko
     */
    public int getKoko() {
        return lauta.length;
    }

    public boolean onShakissa(Side puoli) {
        Kuningas k = etsiKuningas(puoli);
        return k == null || k.olenUhattuna(this);
    }

    public boolean onMatissa(Side vuoro) {
        if (!onShakissa(vuoro)) {
            return false;
        }
        Lista<Siirto> siirrot = generoiSiirrot(vuoro);

        for (int i = 0; i < siirrot.size(); i++) {
            siirto(siirrot.get(i));
            if (!onShakissa(vuoro)) {
                return false;
            }
            peruutaSiirto();
        }

        return true;
    }
}