/* Ohjelmointi Ie harjoitustyö: Ataxx 1/2008 205441 Petteri Aimonen (aimonen) Tekoälykilpailussa haluan esiintyä nimimerkillä jpa. Tekoälyn käyttöä varten lataa esilaskentatiedosto osoitteesta http://www.students.tut.fi/~aimonen/ai.precalc ja pistä samaan hakemistoon kuin missä ohjelma ajetaan. Olemassaoleva tiedosto on pelkkä placeholder testikattavuuden testausta varten. */ #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; }; // Esilaskentatiedoston puurakenne struct AIPuu { unsigned int tyhja; unsigned int oma; unsigned int toinen; }; /* ------------------------- 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. int mahdollisetsiirrot(std::vector &siirrot, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { int maara = 0; Siirto siirto = {0, 0, 0, 0}; int &alkuX = siirto.alkuX; // Lyhyemmät nimet int &alkuY = siirto.alkuY; int &loppuX = siirto.loppuX; 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) { 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 < -1) { 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. // // Esilaskentatiedostossa on AIPuu-tyyppisiä structeja tallennettuna. // Puun tasot vastaavat pelilaudan sijainteja vasemmalta oikealle ja ylhäältä // alas. Structissa on seuraavan puutason sijainti tiedostossa. Viimeisellä // tasolla structiin on koodattu kyseiseen tilanteeseen parhaiten sopiva // siirto. bool ai_hae_esilaskettu(Siirto &tulos, Nappula vuoro, const Nappula pelilauta[LAUDAN_KOKO][LAUDAN_KOKO]) { std::ifstream lahde("ai.precalc"); if (!lahde.good()) { // Tiedostoa ei löytynyt tai ei voitu avata return false; } int syvyys = 0; // Syvyys, jolla puuta ollaan tutkimassa int paikka = 0; // Paikka tiedostossa AIPuu puudata = {0, 0, 0}; // Tallennuspaikka puun datalle Nappula lautadata = TYHJA; // Tallennuspaikka laudalla olevalle nappulalle do { lahde.seekg(paikka * sizeof(AIPuu)); lahde.read(reinterpret_cast(&puudata), sizeof(AIPuu)); lautadata = pelilauta[syvyys % LAUDAN_KOKO][syvyys / LAUDAN_KOKO]; if (lautadata == vuoro) { paikka = puudata.oma; // Oma nappula } else if (lautadata == TYHJA) { paikka = puudata.tyhja; // Tyhjä paikka } else { paikka = puudata.toinen; // Vastustajan nappula } syvyys++; } while (syvyys < LAUDAN_KOKO * LAUDAN_KOKO && lahde.good() && paikka != 0); lahde.close(); if (paikka == 0) { // Siirtoa ei ollut puussa return false; } if (syvyys < LAUDAN_KOKO * LAUDAN_KOKO) { // Tiedosto on loppunut liian aikaisin, sillä lahde.good() ilmeisesti // lopetti loopin. return false; } // Siirto löytyi, puretaan siirto structiin tulos.alkuX = (paikka & 0xFF000000) >> 24; tulos.alkuY = (paikka & 0x00FF0000) >> 16; tulos.loppuX = (paikka & 0x0000FF00) >> 8; tulos.loppuY = paikka & 0x000000FF; return true; } // 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) { char vastaus = 0; do { cout << "Tallennetaanko pelistä replay-tiedosto? (k/e) "; cin >> vastaus; vastaus = std::tolower(vastaus); } while (cin.good() && vastaus != 'k' && vastaus != 'e'); if (vastaus != '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; 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; cout << "### Lataa korvaava tiedosto osoitteesta " \ "http://www.students.tut.fi/~aimonen/ai.precalc" << 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; }