/* Ohjelmointi Ie harjoitustyö: Ataxx 1/2008 205441 Petteri Aimonen (aimonen) Tekoälykilpailussa haluan esiintyä nimimerkillä jpa. Tekoälyn käyttöä varten esilaskentatiedoston ai.precalc on oltava samassa hakemistossa missä ohjelmaa ajetaan. */ #include #include #include #include #include #include using std::cin; using std::cout; using std::endl; /* ------------------------------------------ Ohjelmassa käytetyt datatyypit ja vakiot ------------------------------------------ */ // Pelilaudan koko, 7x7 ruutua const int LAUDAN_KOKO = 7; // "Ääretön" AI-algoritmia varten, suurempi kuin oikeiden siirtovaihtoehtojen // hyvyysluvut. const int AI_INF = 999; // Pelilaudan nappula tai pelaaja enum Nappula {TYHJA, VALKOINEN, MUSTA}; // Nappulan siirto, x ja y pelilauta-taulukon indeksit struct Siirto { int alkuX; int alkuY; int loppuX; int loppuY; }; /* ------------------------- Funktioiden prototyypit ------------------------- */ void alustalauta(Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); void tulostalauta(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); Nappula seuraavavuoro(Nappula nykyinenvuoro); bool luesiirto(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], Nappula vuoro, Siirto &tulos); bool tarkistasiirto(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], Nappula vuoro, const Siirto &siirto); bool tarkistakoordinaatit(int x, int y); int siirronpituus(const Siirto &siirto); void toteutasiirto(Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], const Siirto &siirto); bool peliloppu(Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); void laskepisteet(int &valkoinen, int &musta, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); int mahdollisetsiirrot(std::vector &siirrot, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); void kopioilauta(Nappula kohde[LAUDAN_KOKO][LAUDAN_KOKO], const Nappula lahde[LAUDAN_KOKO][LAUDAN_KOKO]); int laskehyvyys(Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); void ai_etsisiirto(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); bool ai_hae_esilaskettu(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]); int ai_rekursio(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], int syvyys, int alaraja, int ylaraja); /* ------------------------------------------ Perustoimintojen funktioiden toteutukset ------------------------------------------ */ // Alustaa pelilaudan alkutilanteeseen void alustalauta(Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { for (int x = 0; x < LAUDAN_KOKO; x++) { for (int y = 0; y < LAUDAN_KOKO; y++) { pelilauta[x][y] = TYHJA; } } const int n = LAUDAN_KOKO - 1; pelilauta[0][0] = VALKOINEN; pelilauta[n][n] = VALKOINEN; pelilauta[0][n] = MUSTA; pelilauta[n][0] = MUSTA; } // Tulostaa pelilaudan tilanteen näytölle void tulostalauta(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { cout << " 1 2 3 4 5 6 7" << endl; for (int y = 0; y < LAUDAN_KOKO; y++) { cout << y + 1; for (int x = 0; x < LAUDAN_KOKO; x++) { switch (pelilauta[x][y]) { case VALKOINEN: cout << " @"; break; case MUSTA: cout << " O"; break; default: cout << " ."; } } cout << endl; } } // Seuraava vuoro nykyisen vuoron perusteella Nappula seuraavavuoro(Nappula nykyinenvuoro) { if (nykyinenvuoro == VALKOINEN) { return MUSTA; } else { return VALKOINEN; } } // Luetaan siirto käyttäjältä. Palautetaan false jos käyttäjä tahtoo lopettaa // pelin, muutoin true. bool luesiirto(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], Nappula vuoro, Siirto &tulos) { do { cout << "Anna lähtöruudun koordinaatit (x y): "; cin >> tulos.alkuX >> tulos.alkuY; // Muutetaan taulukkoindekseiksi tulos.alkuX -= 1; tulos.alkuY -= 1; if (tulos.alkuX < -1) { // lopetuspyyntö return false; } cout << "Anna kohderuudun koordinaatit (x y): "; cin >> tulos.loppuX >> tulos.loppuY; tulos.loppuX -= 1; tulos.loppuY -= 1; if (!cin.good()) { // EOF, virheellinen syöte, tms. return false; } } while (!tarkistasiirto(pelilauta, vuoro, tulos)); return true; } // Tarkistetaan siirron laillisuus bool tarkistasiirto(const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], Nappula vuoro, const Siirto &siirto) { if (!tarkistakoordinaatit(siirto.alkuX, siirto.alkuY) || !tarkistakoordinaatit(siirto.loppuX, siirto.loppuY)) { cout << "Virhe: Laittomat koordinaatit." << endl; return false; } if (pelilauta[siirto.alkuX][siirto.alkuY] == TYHJA) { cout << "Virhe: Ruudussa ei ole nappulaa." << endl; return false; } if (pelilauta[siirto.alkuX][siirto.alkuY] != vuoro) { cout << "Virhe: Ruudussa oleva nappula ei kuulu vuorossa " \ "olevalle pelaajalle." << endl; return false; } if (pelilauta[siirto.loppuX][siirto.loppuY] != TYHJA || siirronpituus(siirto) > 2) { cout << "Virhe: Laiton siirto." << endl; return false; } return true; } // Tarkistetaan koordinaattien kuuluminen laudalle. bool tarkistakoordinaatit(int x, int y) { if (x < 0 || x >= LAUDAN_KOKO) { return false; } if (y < 0 || y >= LAUDAN_KOKO) { return false; } return true; } // Lasketaan siirron pituus, 1 = kloonaus, 2 = hyppy, muut = laiton int siirronpituus(const Siirto &siirto) { int xero = std::abs(siirto.alkuX - siirto.loppuX); int yero = std::abs(siirto.alkuY - siirto.loppuY); if (yero > xero) { return yero; } else { return xero; } } // Toteutetaan siirto pelilauta-taulukkoon. void toteutasiirto(Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], const Siirto &siirto) { Nappula vari = pelilauta[siirto.alkuX][siirto.alkuY]; pelilauta[siirto.loppuX][siirto.loppuY] = vari; if (siirronpituus(siirto) == 2) { pelilauta[siirto.alkuX][siirto.alkuY] = TYHJA; } /* Tutkitaan 3x3 neliö kohdepaikan ympäriltä ja vaihdetaan värit. */ for (int x = siirto.loppuX - 1; x <= siirto.loppuX + 1; x++) { for (int y = siirto.loppuY - 1; y <= siirto.loppuY + 1; y++) { // Reunapaikoissa vierekkäisiä paikkoja ei välttämättä ole. if (!tarkistakoordinaatit(x,y)) { continue; } if (pelilauta[x][y] != TYHJA) { pelilauta[x][y] = vari; } } } } /* Tarkistetaan pelin loppuminen: - pelilauta täysi - vain yhden pelaajan nappuloita tai - ei mahdollisia siirtoja vuorossaolevalla (bonus) */ bool peliloppu(Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { std::vector temp; if (mahdollisetsiirrot(temp, vuoro, pelilauta) == 0) { return true; } int mustia = 0, valkoisia = 0, tyhjia = 0; for (int x = 0; x < LAUDAN_KOKO; x++) { for (int y = 0; y < LAUDAN_KOKO; y++) { switch(pelilauta[x][y]) { case MUSTA: mustia++; break; case VALKOINEN: valkoisia++; break; default: tyhjia++; } if (mustia > 0 && valkoisia > 0 && tyhjia > 0) { return false; } } } return true; } // Lasketaan valkoisten/mustien nappuloiden määrä laudalla, ja tallennetaan // tulos referenssien kautta. void laskepisteet(int &valkoinen, int &musta, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { valkoinen = 0; musta = 0; for (int x = 0; x < LAUDAN_KOKO; x++) { for (int y = 0; y < LAUDAN_KOKO; y++) { switch(pelilauta[x][y]) { case MUSTA: musta++; break; case VALKOINEN: valkoinen++; break; default: break; } } } } /* ------------------------------------------ Bonustoimintojen funktioiden toteutukset ------------------------------------------ */ // Etsitään mahdolliset siirtovaihtoehdot pelaajalle vuoro. // Jokaista nappulaa kohti tarkastellaan viereiset 5x5 nappulaa. // Mahdolliset kloonaussiirrot tallennetaan vain kerran per kohde, // ei siis eri siirtoa vaikka kloonata voisi useammasta lähteestä, // koska lopputuloshan olisi sama. int mahdollisetsiirrot(std::vector &siirrot, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { int maara = 0; Siirto siirto = {0, 0, 0, 0}; bool kloonauskohteet[LAUDAN_KOKO][LAUDAN_KOKO] = {{false}}; int &alkuX = siirto.alkuX; // Lyhyemmät nimet, jolloin loopit int &alkuY = siirto.alkuY; // sijoittavat koordinaatit suoraan int &loppuX = siirto.loppuX; // structiin. int &loppuY = siirto.loppuY; for (alkuX = 0; alkuX < LAUDAN_KOKO; alkuX++) { for (alkuY = 0; alkuY < LAUDAN_KOKO; alkuY++) { if (pelilauta[alkuX][alkuY] != vuoro) { // Muu kuin oma nappula continue; } for (loppuX = alkuX - 2; loppuX <= alkuX + 2; loppuX++) { for (loppuY = alkuY - 2; loppuY <= alkuY + 2; loppuY++) { if (!tarkistakoordinaatit(loppuX, loppuY)) { // Ei laudalla continue; } if (pelilauta[loppuX][loppuY] != TYHJA) { // Ei tyhjä continue; } if (siirronpituus(siirto) == 1) { // Kloonaus if (kloonauskohteet[loppuX][loppuY]) { // Jo kloonattu tänne continue; } else { kloonauskohteet[loppuX][loppuY] = true; } } // Siirto ok siirrot.push_back(siirto); maara++; } } } } return maara; } // Pelilautataulukon kopiointi, kun kerran memcpy:n käyttö // ei ole suositeltavaa. void kopioilauta(Nappula kohde[LAUDAN_KOKO][LAUDAN_KOKO], const Nappula lahde[LAUDAN_KOKO][LAUDAN_KOKO]) { for (int x = 0; x < LAUDAN_KOKO; x++) { for (int y = 0; y < LAUDAN_KOKO; y++) { kohde[x][y] = lahde[x][y]; } } } // Laskee pelilaudan tilanteen hyvyysluvun, eli montako nappulaa enemmän // vuorossaolevalla pelaajalla on kuin vastustajalla. int laskehyvyys(Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { int valkoinen = 0; int musta = 0; laskepisteet(valkoinen, musta, pelilauta); if (vuoro == MUSTA) { return musta - valkoinen; } else { return valkoinen - musta; } } // Alkusyvyyden välitys ai_rekursiolle // Aluksi tarkistetaan, olisiko esilaskentatiedostossa tilannetta vastaava // siirto. Sen jälkeen tarkistetaan, millainen tilanne tulisi yhden siirron // päästä. Jos tilanne on tarpeeksi hyvä, ei lasketa niin paljoa. void ai_etsisiirto(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { if (ai_hae_esilaskettu(tulos, vuoro, pelilauta) == true) { return; } int hyvyys = ai_rekursio(tulos, vuoro, pelilauta, 1, - AI_INF, AI_INF); int syvyys = 0; if (hyvyys < 2) { syvyys = 4; } else if (hyvyys < 5) { syvyys = 3; } else { syvyys = 2; } ai_rekursio(tulos, vuoro, pelilauta, syvyys, - AI_INF, AI_INF); if (!tarkistasiirto(pelilauta, vuoro, tulos)) { cout << "AI tuotti virheellisen siirron!" << endl; } } // Esilasketun siirron hakeminen. // Ellei tiedostoa löydy tai se on viallinen, käyttäydytään kuin siirtoa // ei olisi tiedostossa, palautetaan false, jolloin se lasketaan itse. // // Esilaskentatiedostoon on laskettu valmiiksi hyvät aloitussiirrot // tietyntyyppisiä vastapelaajia vastaan. Käytännössähän aloitussiirtoja // tarvitaan vain jos vastustaja on niin älykäs, ettei sitä muuten // voitettaisi. // // Tiedosto luetaan uudestaan joka kerta. Käytännössä tämä tuskin hidastaa // paljoa kohtuullisilla tiedostokoilla. // // Jokaisen esilaskentatiedoston rivin alussa on vuorossa olevan pelaajan // merkki, '@' tai 'O'. Seuraavana on laudan tilanne lueteltuna // vasemmalta oikealle ja ylhäältä alas, ja sen jälkeen ko. tilanteessa // vuorossa olevalle pelaajalle paras siirto. bool ai_hae_esilaskettu(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { int x = 0; int y = 0; std::string haettava_vuoro(""); std::ostringstream haettava_lauta; if (vuoro == MUSTA) { haettava_vuoro = "O"; } else { haettava_vuoro = "@"; } // Koostetaan haettava rivin alku for (y = 0; y < LAUDAN_KOKO; y++) { for (x = 0; x < LAUDAN_KOKO; x++) { switch (pelilauta[x][y]) { case MUSTA: haettava_lauta << "O"; break; case VALKOINEN: haettava_lauta << "@"; break; default: haettava_lauta << "."; break; } } } std::ifstream lahde("ai.precalc"); std::string luettu_vuoro(""); std::string luettu_lauta(""); while (lahde.good()) { lahde >> luettu_vuoro >> luettu_lauta; lahde >> tulos.alkuX >> tulos.alkuY; lahde >> tulos.loppuX >> tulos.loppuY; if (luettu_vuoro == haettava_vuoro && luettu_lauta == haettava_lauta.str()) { std::cout << "Precalc hit \\o/" << std::endl; return true; // Tulos on jo tallennettu tulos-structiin } } return false; // Ei löytynyt } // Tekoäly // Etsii seuraavat syvyys siirtoa, vuorotellen oman ja vastustajan siirrot. // Algoritmi on ns. negamax, alpha-beta pruning -optimoinnilla. // Palauttaa omalta kannalta parhaan siirron tulos-viittauksen kautta. int ai_rekursio(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO], int syvyys, int alaraja, int ylaraja) { int paras = - AI_INF; std::vector siirrot; std::vector::const_iterator iter; Nappula uusilauta[LAUDAN_KOKO][LAUDAN_KOKO] = {{TYHJA}}; if (mahdollisetsiirrot(siirrot, vuoro, pelilauta) == 0) { // Peli loppu, tutkitaan voittaja if (laskehyvyys(vuoro, pelilauta) > 0) { // Voitto return AI_INF; } else { // Häviö tai tasapeli return - AI_INF; } } // Jos tutkiminen päättyy, tuotetaan silti edes jokin siirto. // Tämä tulee käyttöön jos AI esim. tajuaa häviävänsä joka tapauksessa. tulos = siirrot.at(0); if (syvyys == 0) { // Hakusyvyys saavutettu, lasketaan pisteet return laskehyvyys(vuoro, pelilauta); } // Tutkitaan siirtovaihtoehdot for (iter = siirrot.begin(); iter < siirrot.end(); iter++) { int hyvyys = 0; Siirto temp = {0, 0, 0, 0}; Nappula uusivuoro = seuraavavuoro(vuoro); kopioilauta(uusilauta, pelilauta); toteutasiirto(uusilauta, *iter); // Hyvyysarvot muutetaan vastaluvuiksi vastustajan siirtoa varten hyvyys = - ai_rekursio(temp, uusivuoro, uusilauta, syvyys - 1, - ylaraja, - alaraja); if (hyvyys > paras) { // Parempi kuin paras tähän mennessä -> tallennetaan paras = hyvyys; tulos = *iter; } if (paras > alaraja) { // Tätä huonompia siirtoja ei tarvitse tutkia alaraja = paras; } if (paras >= ylaraja) { // Löydettiin parempi siirto kuin muissa vaihtoehdoissa, joten // vastustaja ei anna pelin päätyä tähän siirtohaaraan. return paras; } } return paras; } /* ----------------------------------------------------------------- Pääohjelma ja pikkufunktiot, joita käytetään vain pääohjelmasta ----------------------------------------------------------------- */ // Tallentaa pelaajan nimen replaykomentoihin. // Tekee muunnoksen AI -> AI.replay. void tallennanimi(std::ostream &replaykomennot, const std::string &nimi) { if (nimi != "AI") { replaykomennot << nimi << endl; } else { replaykomennot << "AI.replay" << endl; } } // Tallentaa koordinaattiparin replaykomentoihin. // Tekee muunnoksen taulukkoindekseistä 1..7 -välille void tallennakoordinaatit(std::ostream &replaykomennot, int x, int y) { replaykomennot << x + 1 << " " << y + 1 << std::endl; } // Tallentaa replaytiedoston. Parametrina merkkijono, jossa tallennettavat // komennot ovat. Palauttaa true jos ei tallennettu tai tallennettiin // onnistuneesti. Palauttaa false jos tallennuksessa tapahtui virhe. bool tallennareplay(const std::string &replaykomennot) { std::string vastaus(""); char ekamerkki = 0; do { cout << "Tallennetaanko pelistä replay-tiedosto? (k/e) "; cin >> vastaus; ekamerkki = std::tolower(vastaus.at(0)); } while (cin.good() && ekamerkki != 'k' && ekamerkki != 'e'); if (ekamerkki != 'k') { return true; } std::string tiedostonimi(""); cout << "Anna replay-tiedoston nimi: "; cin >> tiedostonimi; std::ofstream tiedosto(tiedostonimi.c_str()); if (!tiedosto.is_open()) { cout << "Virhe: Replay-tiedostoa ei voida kirjoittaa." << endl; return false; } tiedosto << replaykomennot; // Replayta toistettaessa ei tallenneta uudelleen tiedosto << "e" << endl; tiedosto.close(); return true; } // Lukee pelaajien nimet ja tallentaa ne viittausten kautta. // Hoitaa myös tallennuksen replaykomentoihin. void luenimet(std::ostream &replaykomennot, std::string &nimiV, std::string &nimiM) { cout << "Anna valkoisen pelaajan nimi: "; std::getline(cin, nimiV); tallennanimi(replaykomennot, nimiV); cout << "Anna mustan pelaajan nimi: "; std::getline(cin, nimiM); tallennanimi(replaykomennot, nimiM); } // Tarkistaa esilaskentatiedoston olemassaolon ja toiminnan. // Testauksessa luodaan aloitustilanne ja testataan löytyykö se. bool tarkistaesilaskenta() { Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO] = {{TYHJA}}; Siirto temp = {0, 0, 0, 0}; alustalauta(pelilauta); return ai_hae_esilaskettu(temp, VALKOINEN, pelilauta); } int main() { cout << "### OHJ-1101 Ohjelmointi Ie" << endl; cout << "### Petteri Aimonen, 205441, aimonen" << endl; if (!tarkistaesilaskenta()) { cout << "### Esilaskentatiedosto ai.precalc puuttuu " \ "tai on viallinen!" << endl; } // Pelin aloitus std::string nimiV(""), nimiM(""); // Pelaajien nimet std::ostringstream replaykomennot(""); // Replaykomentojen tallennuskohde luenimet(replaykomennot, nimiV, nimiM); Nappula vuoro = VALKOINEN; Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO] = {{TYHJA}}; Siirto siirto = {0, 0, 0, 0}; alustalauta(pelilauta); // Pelin aikana while (!peliloppu(vuoro, pelilauta)) { bool ai = false; // Onko vuorossaoleva pelaaja AI? cout << "Vuorossa: "; if (vuoro == VALKOINEN) { ai = (nimiV == "AI"); cout << nimiV << endl; } else { ai = (nimiM == "AI"); cout << nimiM << endl; } tulostalauta(pelilauta); if (!ai) { if (luesiirto(pelilauta, vuoro, siirto) == false) { // Käyttäjä tahtoo lopettaa tallennakoordinaatit(replaykomennot, siirto.alkuX, siirto.alkuY); break; } } else { ai_etsisiirto(siirto, vuoro, pelilauta); } toteutasiirto(pelilauta, siirto); tallennakoordinaatit(replaykomennot, siirto.alkuX, siirto.alkuY); tallennakoordinaatit(replaykomennot, siirto.loppuX, siirto.loppuY); vuoro = seuraavavuoro(vuoro); } // Pelin loputtua int pisteetV = 0, pisteetM = 0; tulostalauta(pelilauta); laskepisteet(pisteetV, pisteetM, pelilauta); if (pisteetV == pisteetM) { cout << "Peli päättyi tasapeliin "; } else if (pisteetV > pisteetM) { cout << nimiV << " voitti "; } else if (pisteetM > pisteetV) { cout << nimiM << " voitti "; } cout << pisteetV << "-" << pisteetM << "!" << endl; // Replay-tiedosto tallennareplay(replaykomennot.str()); return EXIT_SUCCESS; }