16
Ara
2025

C++ ile TicTacToe oyunu

Bu yazıda TicTacToe oyunu C++ ile kodlanmıştır. Oyunda insan-bilgisayar karşılıklı oynamaktadır. Orta zorluk seviyesinde bir algoritma kodlamak için Min-max algoritmasının bir varyasyonu oluşturulmuştur. Min-max algoritması, yapay zekada, özellikle oyun teorisinde ve bilgisayar oyunlarında kullanılan bir karar verme algoritmasıdır. En kötü senaryoda (min) olası kaybı en aza indirmek ve potansiyel kazancı en üst düzeye çıkarmak (max) tasarlanmıştır.  Algoritma olası her adım için sonraki tüm senaryoları değerlendirip en iyi hamleyi yapmaya çalışır. Temel amaç mümkünse kazanmak, mümkün değilse en azından kaybetmemektir (yani en kötü ihtimalle beraberlik).

Min-max algoritması aşağıdaki adımlardan oluşur:

1.Oyun ağacını oluştur

Sıradaki hamleden itibaren sonraki olası tüm hamleleri içeren bir ağaç yapısı oluşturur. Örnek bir oyun ağacı aşağıdaki gibidir. Bilgisayar, oyun tahtasının mevcut durumuna göre kendisinin ve rakibinin sırasıyla olası tüm hamlelerini skorlar. Daha sonra her seviyede bilgisayarın hamlelerinin maksimum, rakibin hamlelerinin minimum değerleri alınarak bir üst seviyeye taşınır. Ağacın köküne ulaşana kadar işlem tekrar edilir ve hangi hamlenin yapılacağına karar verilir.

Bu yazıda paylaşılan kodda orta seviye zorlukta bir oyun oluşturmak için her seviyede elde edilen skor log10‘a göre katsayılandırılmıştır.

2.Terminal durumunu kontrol et

Algoritma, oyunun bittiği her durumu bir terminal olarak ele alır. TicTacToe oyununda aşağıda resimde gösterildiği üzere 3 terminal durum vardır:

  1. O kazanır
  2. Berabere biter
  3. X kazanır

Min-max algoritması bir terminal düğüme ulaştığında, artık o düğümün alt ağaçları ile ilgilenmez. Terminal düğümün skoru bir üst düğüme taşınır. Algoritma, olası tüm terminallere ulaşır ve bilgisayarın kazanma durumlarına +1, beraberliğe 0, kaybetme durumuna -1 puan skor verir.

3.Yapraklardan yukarı doğru değerleri yay

Bir düğümün çocuklarında aynı derinlik seviyesinde elde edilen skorlar bir üst düğüme şu kural ile taşınır:

  • Hamle sırası rakipte ise minimum değer taşınır.
  • Hamle sırası bilgisayarda ise maksimum değer taşınır.

4.En uygun hamleyi seç

Algoritma, tüm olası hamlelerin skorlarını hesapladıktan sonra en yüksek skorlu hamleyi seçip oynar.

Fonksiyonlar

Aşağıda, kaynak kodda kullanılan global fonksiyon ve değişkenlerin açıklamaları verilmiştir.

  • rasgeleOyna(): Oyunun hemen başında oyun tahtası boş olduğu için min-max algoritması kullanmaya gerek yoktur. Bu fonksiyon ile oyun başlangıcında bilgisayarın ilk hamlesini rasgele boş bir hücreye yapması sağlanır.
  • yonerge(): Oyunun başlangıcında oyunun oynanışı ile ilgili yönergeyi gösterir. Örneğin; tahtanın hücreleri 1-9 arası numaralandırılmıştır. Kullanıcı oynarken hücre numarasını tuşlar.
  • basla(): Yeni bir oyun başlatır ve boş tahtayı hazırlar.
  • tahtayiYazdir(): Oyun tahtasının son durumunu ekrana yazdırır.
  • siraOyuncuda(): Oyun sırası kullanıcıda iken çalışan işlemleri içerir. Fonksiyon, kullanıcıdan bir hücreye oynamasını ister. Kullanıcı dolu bir hücreye oynamak isterse veya yanlış bir karakter tuşlarsa hata gösterir.
  • siraBilgisayarda(): Oyun sırası bilgisayarda iken çalışan işlemleri içerir. Bilgisayar, arka planda sırayla tüm boş hücrelere hamle yapar ve oyun ağaçlarını oluşturur. Bunun için skorTahtasi[3][3]değişkenini kullanarak olası her hamle için skor belirler. Amaç matematiksel olarak sıradaki en iyi hamleyi bulmaktır. skorTahtasi[3][3]değişkenindeki en yüksek skor en iyi hamle kabul edilir.
  • terminalMi(): Kendisine gönderilen oyun tahtasının terminal olup olmadığını kontrol edip sonucunu döndürür.
  • kazananKontrol(): Kendisine gönderilen oyun tahtasında bir kazanan var mı kontrol eder.
  • maxSkorHesapla(): Oyun ağacının ilgili düğümü için bir skor oluşturur. Bu skoru oluştururken, söz konusu hamleden itibaren tüm terminallere ulaşana kadar sırayla maxSkorHesapla() ve minSkorHesapla() fonksiyonları birbirini çağırır. Oyun ağacının kökünden uzaklaştıkça terminallerin skora etkisi azalır.
  • minSkorHesapla(): Oyun ağacnın bir seviyesinde kullanıcının olası hamlesi için bir skor oluşturur. Kullanıcının olası her hamlesi için bilgisayarın hedefi oyunu kaybetmemektir. Oyunu kaybeden bir terminal için bilgisayar yüksek skor üreterek kullanıcının bu hamlesi engellenmek ister. Bir başka deyişle, kullanıcının hamle yaparak kazanacağı bir hücreye bilgisayar daha önce hamle yapmalı ve kullanıcının kazanmasını engellemelidir. Burada da oyun ağacının derinliğine inildikçe terminallerin skora etkisi azalır.

Bazı önemli değişkenler

  • char tahta[3][3]: Oyun tahtasıdır. Oyuncunun hamlesi ‘O’, bilgisayarın hamlesi ‘X’, boş hücre boşluk karakteri olarak tutulur.
  • bool tahtaBos: True ise henüz tahtada kimsenin hamle yapmamıştır.
  • bool oyunBitti: Oyun tahtasının bir terminale ulaştığını, yani oyunun birinin kazanması veya beraberlik durumu ile sonlandığını gösterir.
  • int derinlik: Min-max algoritmasının skor üretirken oyun ağacının hangi seviyesinde olduğunu gösterir. Derine indikçe hamlenin skora etkisi azalır.

Ekran görüntüsü

Kaynak kod

// hbmacit.tictactoe
// Bilgisayar "X", kullanıcı "O" oynar. 

#include<iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

char tahta[3][3];
int skorTahtasi[3][3]; // Min maks algoritmasının elde ettiği sonuçları tutacak.
bool tahtaBos = true;
bool oyunBitti = true;
int derinlik;

int MinSkorHesapla(char gelenTahta[3][3], int gelenSkor, int gelenDerinlik);
int MaxSkorHesapla(char gelenTahta[3][3], int gelenSkor, int gelenDerinlik);
int kazananKontrol(char gelenTahta[3][3]);

void rasgeleOyna(){
    int sat, sut;
    if (!tahtaBos){ // Tahta boş değil, yani ilk el oynanmış
        do{
            srand((unsigned)time(0));
            sat = (rand()%3);
            sut = (rand()%3);
        }while (tahta[sat][sut] != ' ');
        tahta[sat][sut] = 'X';
    }
    else    // Tahta boş, yani en ilk el oynanacak
    {
        srand((unsigned)time(0));
        sat = (rand()%3);
        sut = (rand()%3);
        tahta[sat][sut] = 'X';
    }
    cout<<"Bilgisayar "<<(sat*3)+sut+1<<" oynadı."<<endl;
    derinlik--;
}

void yonerge(){
    cout<<"hbmacit.tictactoe 2025"<<endl;
    int siraNo = 1;
    cout<<"-------------"<<endl;
    for (int i=0;i<3;i++){
        cout<<"| ";
        for(int j=0;j<3;j++){
                cout<<siraNo;
                cout<<" | ";
                siraNo++;
            }
        cout<<endl<<"-------------"<<endl;
        }
}

void basla(){
    // Tahtayı sıfırla
    oyunBitti = false;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            tahta[i][j] = ' ';
            skorTahtasi[i][j] = 0;
        }
    tahtaBos = true;
}

void tahtayiYazdir(){
    cout<<"-------------"<<endl;
    for (int i=0;i<3;i++){
        cout<<"| ";
        for(int j=0;j<3;j++){
            cout<<tahta[i][j];
            cout<<" | ";
        }
        cout<<endl<<"-------------"<<endl;
    }
}

void siraOyuncuda(){
    int siraNo, sat = 0, sut=0;
    if (!oyunBitti){
        cout<<"Oynamak için boş bir hücre no giriniz."<<endl;
        bool dogruHamle = false;
        do{
            // Kullanıcının girdiği sıra no hangi satır ve sütunu ifade ediyor?
            cin>>siraNo;
            if ((siraNo>0)&&(siraNo<10)){ // 1-9 arası bir sayı girildiyse işleme al
                sut = (siraNo-1) % 3;
                if (siraNo<=3) sat = 0;
                if ((siraNo>3)&&(siraNo<=6)) sat = 1;
                if (siraNo>6) sat = 2;
                if (tahta[sat][sut]==' '){
                    dogruHamle = true; // Boş bir hücre girildi
                }
            }
            if (dogruHamle==false){
                tahtayiYazdir();
                cout<<"Hatalı giriş. Oynamak için boş bir hücre no giriniz."<<endl;
            }
            // Girilen sıra no'nun hücresi boş mu?
        }while(dogruHamle==false);
        tahta[sat][sut]='O';
        tahtayiYazdir();
        tahtaBos = false;
        derinlik--;
    }
}

void siraBilgisayarda(){
    int skor = 0; // Sıradaki hamlenin etki değerini tutacak değişken
    int sat=0;int sut=0;
    // oyun tahtasının bir kopyasını oluşturup min-maks'a göndereceğiz.
    char kopyaTahta[3][3];
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            // Orijinal tahta bozulmasın diye oyun tahtasının bir kopyasını min-maks'a göndereceğiz
            for(int k=0;k<3;k++)
                for(int l=0;l<3;l++){
                    kopyaTahta[k][l] = tahta[k][l];
                }

            if(kopyaTahta[i][j]==' '){
                kopyaTahta[i][j]='X'; // Mesela buraya oynadık varsayalım
                if (kazananKontrol(kopyaTahta)==1){
                    skor+=(10^derinlik);
                }
                skor += MinSkorHesapla(kopyaTahta,skor,derinlik);
                
            }
            skorTahtasi[i][j] = skor;
            skor = 0;
        }
    // Skor tahtası çıkarıldı, maksimum değeri olan hücreleri bul ve birine 'O' koy.
    // Bunun için maksimum hücrelerin bulunduğu yeri tutan bir kopyaMaksSkor tahtası oluştur.
    int maksSkor = -1*(1e9);;
    int kopyaMaksSkor[3][3];
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            kopyaMaksSkor[i][j] = -1*(1e9); // Maksimum olmayan hücreler -10^9 olsun
            if (tahta[i][j]==' ')
                if (maksSkor < skorTahtasi[i][j]){
                    maksSkor = skorTahtasi[i][j]; // Yeni maksimum
                
                }
        }
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            if (skorTahtasi[i][j] >= maksSkor){
                kopyaMaksSkor[i][j] = maksSkor; // Yeni maks olan hücreleri belirle (Birden fazla olabilir)
            }
        }

    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            if (kopyaMaksSkor[i][j] >= maksSkor)
                if (tahta[i][j]==' '){
                    sat = i; sut = j;
                    break;
            }            
        }
    
    // Sat ve Sut'ün (satır-sütun) işaret ettiği hücreye oyna
    tahta[sat][sut] = 'X';
    cout<<"Bilgisayar "<<(sat*3)+sut+1<<" oynadı."<<endl;
    derinlik--;
    tahtayiYazdir();
    tahtaBos = false;
}

// Bir tahtanın terminal olup olmadığını kontrol et
bool terminalMi(char gelenTahta[3][3]){
    bool bosHucreBulundu = false;
    // Boş hücre var mı?
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            if (gelenTahta[i][j]==' ') bosHucreBulundu = true;
        }
    if (bosHucreBulundu){
        return false;
        
    }else{
        return true;
    }
}

// Kazanan var mı kontrol et.
// Bilgisayar kazandı ise +1, kullanıcı kazandı ise -1, kazanan yoksa 0 döndür.
int kazananKontrol(char gelenTahta[3][3]){
    // Bilgisayarın kaybettiği durumlar:
    if ((gelenTahta[0][0]=='O')&&(gelenTahta[0][1]=='O')&&(gelenTahta[0][2]=='O')) return -11;
    if ((gelenTahta[1][0]=='O')&&(gelenTahta[1][1]=='O')&&(gelenTahta[1][2]=='O')) return -1;
    if ((gelenTahta[2][0]=='O')&&(gelenTahta[2][1]=='O')&&(gelenTahta[2][2]=='O')) return -1;
    if ((gelenTahta[0][0]=='O')&&(gelenTahta[1][0]=='O')&&(gelenTahta[2][0]=='O')) return -1;
    if ((gelenTahta[0][1]=='O')&&(gelenTahta[1][1]=='O')&&(gelenTahta[2][1]=='O')) return -1;
    if ((gelenTahta[0][2]=='O')&&(gelenTahta[1][2]=='O')&&(gelenTahta[2][2]=='O')) return -1;
    if ((gelenTahta[0][0]=='O')&&(gelenTahta[1][1]=='O')&&(gelenTahta[2][2]=='O')) return -1;
    if ((gelenTahta[0][2]=='O')&&(gelenTahta[1][1]=='O')&&(gelenTahta[2][0]=='O')) return -1;
    // Bilgisayarın kazandığı durumlar
    if ((gelenTahta[0][0]=='X')&&(gelenTahta[0][1]=='X')&&(gelenTahta[0][2]=='X')) return 1;
    if ((gelenTahta[1][0]=='X')&&(gelenTahta[1][1]=='X')&&(gelenTahta[1][2]=='X')) return 1;
    if ((gelenTahta[2][0]=='X')&&(gelenTahta[2][1]=='X')&&(gelenTahta[2][2]=='X')) return 1;
    if ((gelenTahta[0][0]=='X')&&(gelenTahta[1][0]=='X')&&(gelenTahta[2][0]=='X')) return 1;
    if ((gelenTahta[0][1]=='X')&&(gelenTahta[1][1]=='X')&&(gelenTahta[2][1]=='X')) return 1;
    if ((gelenTahta[0][2]=='X')&&(gelenTahta[1][2]=='X')&&(gelenTahta[2][2]=='X')) return 1;
    if ((gelenTahta[0][0]=='X')&&(gelenTahta[1][1]=='X')&&(gelenTahta[2][2]=='X')) return 1;
    if ((gelenTahta[0][2]=='X')&&(gelenTahta[1][1]=='X')&&(gelenTahta[2][0]=='X')) return 1;
    else return 0; // Kimse kazanmamış - beraberlik
}

// Bilgisayar için hamlenin maksimum skorunu bul
int MaxSkorHesapla(char gelenTahta[3][3], int gelenSkor, int gelenDerinlik){
    // Kazanan var mı kontrol et.
    if (kazananKontrol(gelenTahta)!=0){ // Bir kazanan var, skoru döndür.
        return (kazananKontrol(gelenTahta)*(10^gelenDerinlik)+gelenSkor);
    }else if (terminalMi(gelenTahta)==true){  // Kazanan yok ve terminale ulaştık. Mevcut skoru döndür.
        return gelenSkor;
    }else{
        // Kazanan yok, ara düğümdeyiz. Algoritmaya devam et.
        // Sıradaki Boş hücreyi bul ve bu hücrenin skorunu hesapla
        for(int i=0;i<3;i++)
            for(int j=0;j<3;j++){
                if(gelenTahta[i][j]==' '){
                    gelenTahta[i][j]='X'; // Mesela buraya oynadık varsayalım.
                    gelenSkor += (MinSkorHesapla(gelenTahta,gelenSkor,gelenDerinlik-1));                    
                }
            }
        return gelenSkor;
    }
}

// Oyuncu için hamlenin minimum skorunu bul
int MinSkorHesapla(char gelenTahta[3][3], int gelenSkor, int gelenDerinlik){
    // Kazanan var mı kontrol et.
    if (kazananKontrol(gelenTahta)!=0){ // Bir kazanan var, skoru döndür.
        return (kazananKontrol(gelenTahta)*(10^gelenDerinlik)+gelenSkor);
    }else if (terminalMi(gelenTahta)==true){ // Kazanan yok ve terminale ulaştık. Mevcut skoru döndür.
        return gelenSkor;
    }else{
        // Kazanan yok, ara düğümdeyiz. Algoritmaya devam et.
        // Sıradaki Boş hücreyi bul ve bu hücrenin skorunu hesapla
        for(int i=0;i<3;i++)
            for(int j=0;j<3;j++){
                if(gelenTahta[i][j]==' '){
                    gelenTahta[i][j]='O'; // Mesela buraya oynadık varsayalım.
                    
                    if (gelenDerinlik == derinlik) {// İlk iterasyon, hemen kaybetme ihtimali var mı?
                        //cout<<"oyun gidiyor!.......";
                        if (kazananKontrol(gelenTahta) == -1){
                            return 2*(10^gelenDerinlik)+gelenSkor;
                        }
                    }
                    gelenSkor += (MaxSkorHesapla(gelenTahta,gelenSkor,gelenDerinlik-1));
                }
            }
        return gelenSkor;
    }
}

int main(){
    yonerge();
    basla();
    derinlik = 9; // Kalan hamle sayısı (Oyun ağacının derinliği)
    
    // Kim başlayacak? Rasgele seç
    srand((unsigned)time(0));
    int secici = (rand()%100);
    bool oyuncuOynayacak;
    if (secici%2 == 1) oyuncuOynayacak = true;
    else oyuncuOynayacak = false;
    
    
    if (!oyuncuOynayacak){ // İlk hamle bilgisayarın
        cout<<"Bilgisayar başlıyor! ";
        rasgeleOyna();
        tahtayiYazdir();
        siraOyuncuda();
    }else{  // İlk hamle kullanıcının 
        cout<<"Oyuncu başlıyor! ";
        siraOyuncuda();
        rasgeleOyna();
        tahtayiYazdir();
    }
    
    // Oyun bitene kadar sırayla kullanıcı ve bilgisayar oynasın
    while(kazananKontrol(tahta)==0){
        if (terminalMi(tahta)==true){
            cout<<"Berabere bitti.";
            break;
        }else{
            if (oyuncuOynayacak){
                cout<<"Oyuncu bekleniyor...";
                siraOyuncuda();
            } else{
                cout<<"Bilgisayar bekleniyor...";
                siraBilgisayarda();
            }
            oyuncuOynayacak = !oyuncuOynayacak;
        }
    }
    if (kazananKontrol(tahta)==1){
        cout<<"Bilgisayar kazandı.";
    }else if (kazananKontrol(tahta)==-1){
        cout<<"Oyuncu kazandı.";
    }
    return 0;
}