Bir önceki dersimizde oyunlardaki yapay zekâya giriş yapmıştık. Sonrasında sizlere sonlu durum makinelerinden(Finite state machine ) bahsetmiştim. Şimdi de ilk makalenin devamı olacak şekilde, sonlu durum(state) makinelerinin oluşturulması ve kullanılmasını anlatacağım, yani bu makale genel itibariyle uygulama esaslı olacak.
Sonlu durum makinelerini anlatmak için basit bir uygulama seçtim. Örneğimizde bir tavuğu ve davranışlarını işleyeceğiz. Tavuğun, yem yeme, su içme ve yumurtlama olmak üzere üç hali(state) olacak. Tavuğumuz acıkınca yem yemeye, susayınca su içmeye ve yumurtası gelince de yumurtlamaya gidecek. Gördüğünüz gibi tavuk susadığını veya acıktığını bilebiliyor, zekâ belirtisi gösteriyor. Tavuğun durumlarını(state) aşağıdaki gibi ifade edersek:
YAPAY ZEKÂ 2
Bir önceki dersimizde oyunlardaki yapay zekâya giriş yapmıştık. Sonrasında sizlere sonlu durum makinelerinden(Finite state machine ) bahsetmiştim. Şimdi de ilk makalenin devamı olacak şekilde, sonlu durum(state) makinelerinin oluşturulması ve kullanılmasını anlatacağım, yani bu makale genel itibariyle uygulama esaslı olacak.
Sonlu durum makinelerini anlatmak için basit bir uygulama seçtim. Örneğimizde bir tavuğu ve davranışlarını işleyeceğiz. Tavuğun, yem yeme, su içme ve yumurtlama olmak üzere üç hali(state) olacak. Tavuğumuz acıkınca yem yemeye, susayınca su içmeye ve yumurtası gelince de yumurtlamaya gidecek. Gördüğünüz gibi tavuk susadığını veya acıktığını bilebiliyor, zekâ belirtisi gösteriyor. Tavuğun durumlarını(state) aşağıdaki gibi ifade edersek:

Peki tavuğumuz yumurtasının geldiğini veya susadığını nasıl anlayacak ? Şu şekilde bir örnekle ifade edelim . Susadığını anlayabilmesi için susuzluk seviyesini gösteren ve onu sorgulayan bir örnek:
int susuzlukSev = 5 ;
if( tavuk.susuzlukSeviyesi() > susuzlukSev )
cout << “ susadım ve su içmek istiyorum ”;
peki tavuğun susamasının yem yerken veya yumurtlarken mi olduğunu nasıl bileceğiz? Şayet yumurtlarken başka yem yerken başka bir davranış - parlak zekâ – göstermesini istiyorsak şöyle kodlamak zorundayız:
class Hal{};
int susuzlukSev = 5 ;
Hal tavukHali ;
if(tavukHali == yemekyiyor )
{
if( tavuk.susuzlukSeviyesi() > susuzlukSev )
cout << “ susadım ve hemen su içmek istiyorum , ”;
}else
if(tavukHali == yumurtluyor )
if( tavuk.susuzlukSeviyesi() > susuzlukSev )
cout << “ susadım ve su içmek istiyorum ama yumurtlamayı bırakamam ”;
Örnekte gördüğünüz gibi tavuğun susadığı durumlar(state) için o anki haline göre tepki vermek istiyorsak her durum(state) için ayrı sorgulama yapmak zorundayız. Tepkisini duruma(state) göre belirlemek zorundayız. Her durum(state) içi ayrı kodlama yapmak ve tavuğun hangi halde olduğunu sorgulamak işleri zorlaştırır .Hem bu şekilde program esnek olmaz , yeni durumların(state) eklenmesi işleri içinden çıkılmaz hale getirir.Tavuk için yeni durumlar(state) tanımlamak istiyorsak kodu yeniden yazmak zorunda kalırız.Böyle bir kodlama mantığı yanlış olur , yerine tavuğun durumlarını(state) ayrı birer varlık yaparsak : Tavuk müstakil bir varlık olarak temsil edilen duruma girse ve o durumda tanımlı görevleri , o durumun şartları içinde yapmak zorunda olsa?Böyle bir yöntem çok daha mantıklı olur.Hem tavuğun gireceği durumların görevleri bellidir hem de tavuk o duruma girdiği anda yapmak zorunda olduğu işleri otomatikman yapar.Mesela asker saldırı durumuna geçerse silahını doğrultur.Bu silah doğrultma işi saldırı durumuna geçildiğinde otomatikman yapılacaktır.Kaçma durumunda ise silahını indirme işi otomatikman yapılır.Tavuk da yem yeme durumuna geçtiğinde mekanını yemlik olarak değiştirir. Gördüğünüz gibi durumları ayrı varlıklar olarak tanımlamak daha mantıklı olur. Şimdi bu durumlar nasıl tanımlanıyor ona gelelim.
Aşağıdaki kod, soyut temel sınıf ( abstract base class ) özelliğinde oluşturulmuş temel bir sınıftır. Sınıfta saf sanal(pure virtual ) fonksiyonlar olduğu için örneği üretilemez. Zaten kendinden türetilecek sınıflara arayüz sağlamak için yazdım , örneğini oluşturmak için değil. . Tavuğun durumları bu temel durumdan türetilecektir.
class Durum
{
public:
virtual ~Durum(){}
virtual void Gir( Tavuk * ) = 0;
virtual void DurumGorevleri( Tavuk * ) = 0;
virtual void Cik( Tavuk * ) = 0;
};
Uygulamamızda kullanacağımız durumlarda(state) yukarıdaki sınıfta tanımlandığı şekilde fonksiyonlar tanımlanmalıdır. Türetilen fonksiyonlar singleton class özelliğine sahip olacak ve programdaki tek örnekler olacaktır.
Genel anlamda durumlarımızda üç fonksiyon var. Her durum değişiminde bu üç fonksiyon çağrılmalıdır. Bu fonksiyonlar çağrıldığında durumun kendine has özellikleri yapay zeka tepkilerine yansıyacaktır. Mesela tavuk su içme durumuna girmişse mekânını suluk olarak değiştirecek ; DurumGörevleri fonksiyonunda açlığını bastıracak ; durumdan çıktığında ise duruma göre bir iş yapacaktır. Gördüğünüz gibi durumlar çok daha düzgün bir şekilde ifade edilebiliyor. Ayrıca yeni durum ekleme de kolaylaşıyor.
Bu durumlara girme işi duruma girecek varlığın fonksiyonlara gönderilmesi ile olur. Biz de yukarıdaki örnekte Tavuk sınıfının bir örneğini gönderiyoruz. Verdiğim örnekte 300 satır kod var, haliyle her satırı ayrı olarak açıklamadım. Anlattıklarımdan geri kalan kısmı kodları inceledikçe anlayacaksınız.
Şimdi bu temel sınıftan tavuğumuzun durumlarını oluşturalım. Örnek olarak YemYemeDurumu sınıfını oluşturalım.
class YemYemeDurumu: public Durum
{
private:
YemYemeDurumu() {}//default constructor
YemYemeDurumu(const YemYemeDurumu& );//copy constuctor
YemYemeDurumu& operator=(const YemYemeDurumu &) ; // atama operatörü
public :
static YemYemeDurumu * alTekOrnek() ;
virtual void Gir( Tavuk * ) ;
virtual void DurumGorevleri( Tavuk * );
virtual void Cik( Tavuk * );
};
Bu Durum class ın dan türetilmiştir ve sınıfımız singleton class özelliğindedir . Dolayısıyla tek örneği üretilecektir. Birden fazla örnek üretimini engellemek için default constructor u private tanımladım, dışarıdan çağrılamayacak.Derleyici copy constructor üretmesin diye de kendimiz bir copy constructor tanımlıyoruz , yalnız sadece tanımlı olarak kalacak , kodlamayacağız.Aynı şekilde atama operatörünü de biz tanımlayalım ki derleyici bizim yerimize tanımlamasın.Bizim haberimiz olmadan programda yeni bir durum üretilmesin.
Bu Durumun( YemYemeDurumu ) kodlanması :
YemYemeDurumu * YemYemeDurumu::alTekOrnek()
{
static YemYemeDurumu tekOrnek ;
return &tekOrnek ;
}
Sınıfımız singleton class olduğu için sınıfın tek örneği üretilebilir.O örnek de static olarak tanımlanmış örneğin istenilmesi durumunda , çağrılan static fonksiyon vasıtasıyla üretilir.Sonraki çağırışlarda Durum örneği yeniden üretilmez.
void YemYemeDurumu::Gir( Tavuk *tavuk )
{
if(tavuk->alMekan() != yemlik )
tavuk->yerles( yemlik );
}
Tavuk bu duruma girdiğinde mekanını denetliyor, şayet yemlikte değilse mekânını otomatikman değiştiriyor ve yemliğe geçiyor.
void YemYemeDurumu::DurumGorevleri(Tavuk *tavuk )
{
tavuk->artirSusuzlukSev() ;// yem yerken susuyor
tavuk->kurAclikSev( 0 ) ; // yem yediği için açlığı bitti
cout << "su an :" <<MekaniYaziyaCevir( tavuk->alMekan() )<<"teyim" ;
cout << "acligimi gideriyorum" << endl;// ne yaptığını bize söylüyor
if(tavuk->alSusuzlukSev() >= SusuzlukSeviyesi )
tavuk->DurumDegistir( SuIcmeDurumu::alTekOrnek() ) ;
if(tavuk->alYumurtaSev() >= YumurtaSeviyesi )
tavuk->DurumDegistir( YumurtlamaDurumu::alTekOrnek() ) ;
}
Yukarıda kullanılan, tavuk nesnesinden çağırdığımız fonksiyonları az sonra anlatacağım. Burada yapmak istediğim: Bu duruma girildiğinde yapılacak görevleri anlatmaktır. MekaniYaziyaCevir(enum mekan) fonksiyonu Mekanlar.h dosyasında tanımlı bir fonksiyondur.
YemyemeDurumu’nda ilk olarak susuzluğumuz artacaktır. Sonra yem yediğimiz için açlık seviyemiz sıfırlanacaktır. Sonra nerede olduğunu yazıyoruz ki karakteri takip edebilelim. En son olarak susuzluğu kontrol ediyoruz , şayet susamışsa durumu SuIcmeDurumu’na değiştiriyoruz, yumurta gelmişse de tavuğu YumurtlamaDurumu na getiriyoruz.Dikkat ederseniz susuzluğun önceliği var.
void YemYemeDurumu::Cik( Tavuk * tavuk )
{
cout << MekaniYaziyaCevir( tavuk->alMekan() )<< " terkediyorum" << endl;
}
Bu durumda tavuğun yaptığı , faaliyetini takip edebilmemiz için yaptıklarını bize bildirmesidir.
Buraya kadar örnek bir durum incelemesi yaptık.Şimdi ise oyun karakterimizin nasıl üretildiğini inceleyelim.
Oyun karakterimiz tavuk aşağıdaki sınıftan türetilir.
class OyunKarakteri
{
private:
int u_KN ; // kimlik no su , uye_KimlikNo dan kısaltma
static int gecerliKN ; //bir sonra üretilecek karakterin kimlik no su
void kurKN( int kn ) ;
public:
OyunKarakteri( int kn )
{
kurKN( kn );
}
virtual ~OyunKarakteri(){}
virtual void Guncelle() = 0 ;//saf sanal fonksiyon
int alKN() const { return u_KN ; }
};
Tavuk sınıfımız bu sınıftan türetilecektir.Zaten bu sınıfın örneği türetilemez çünkü bu sınıf Guncelle isimli saf sanal(pure virtual) fonksiyonu bulunan bir sınıftır.Güncelle fonksiyonu içinde ait olunan duruma(state) göre sorgulamalar yapacak , bu da bize yapay zeka güncellemesi olarak yansıyacaktır.
Sınıfın , gecerliKN isimli static bir değişken içermesinin sebebi uygulamamızda birden fazla karakter olabileceğidir.Her karakteri ait olduğu kimlik no suna göre ayırt edebiliriz.yalnız bu örnekte tek bir tavuk nesnesi ürettim.
Şimdi bu sınıftan türetilen Tavuk sınıfına gelelim.
class Tavuk: public OyunKarakteri
{
private:
Durum * u_Durum ; // her karakterin bir durumu olacak
mekan u_Mekan ;
int u_SusuzlukSev ; // susuzluk seviyesi
int u_AclikSev ;
int u_YumurtaSev ;
int u_YumurtaGeldi ;
public:
Tavuk( int kn ) ;
void Guncelle() ;
void DurumDegistir( Durum *yeniDurum) ;
// uye degerlerin alınması için gerekli fonksiyonlar
int alSusuzlukSev(){ return u_SusuzlukSev ; }
int alAclikSev(){ return u_AclikSev ; }
int alYumurtaSev(){ return u_YumurtaSev ; }
int alYumurtaGeldi(){ return u_YumurtaGeldi ; }
mekan alMekan(){ return u_Mekan ; }
// degerler atanması
void kurYumurtaSev(int sev){ u_YumurtaSev = sev ; }
void kurSusuzlukSev( int sev ){ u_SusuzlukSev = sev ;}
void kurAclikSev( int sev ){ u_AclikSev = sev ;}
void kurYumurtaGeldi(int sev){ u_YumurtaGeldi = sev ;}
//üye değerlerin artırılması
void artirAclikSev( ){ u_AclikSev++ ;}
void artirYumurtaSev( ){ u_YumurtaSev++ ;}
void artirSusuzlukSev( ){ u_SusuzlukSev++ ;}
void artirYumurtaGeldi(){ u_YumurtaGeldi++ ; }
void yerles( mekan m){ u_Mekan = m ; }
// ac-susuz olup olmadığının anlaşılması için gerekli fonksiyonlar
bool acMi() const ; // tavuk aç mı?
bool susuzMu() const ;
bool yumurtaGeldiMi() const ;
};
Bu sınıfta ilginç olan her tavuk nesnesinin kendine ait durumu(state) olmasıdır.Bu Durum(state) değişken bize yapay zekayı güncelleme işinde yardımcı olacaktır.Her Güncelle fonksiyonun çağrımında duruma(state) göre yapay zekayı oluşturan fonksiyon ( durumgörevleri )çağrılır.DurumDegistir fonksiyonu ise durum değiştirmek durumunda kalınırsa çağrılan bir fonksiyondur.Bu fonksiyon state teki , duruma girme ve durumdançıkma fonksiyonlarını çağıran bir fonksiyondur.
Her ne kadar bütün kodları anlatmasam da önemli kısımlarını anlattım.Kodları incelediğiniz zaman aklınıza takılan yerleri anlayacaksınız.Anlayamadığınız yer olursa da sorunuzu www.karabit.org sitesinde sorabilir veya özel e-mektup adresimden(
Bu e-Posta adresi istek dışı postalardan korunmaktadır, görüntülüyebilmek için JavaScript etkinleştirilmelidir
) bana ulaşabilirsiniz.
Yazı hakkındaki eleştirilerinizi merakla bekliyorum.Eksik kalan yerler varsa makaleyi yeniden güncelleyebilirim.Kodların hepsini Microsoft Visual C++ .NET 7.1 sürümündeki C++ derleyicisi ile derledim . Derleyicinizin sorun çıkaracağını sanmıyorum.Yine de hatalar oluşursa lütfen bana bildirin.
Ahmet Arslan 17-07-2006
www.oyungelistirici.net'te Alparslan lakabını kullanıyorum , sorularınızı oradan sorabilirsiniz.Özelden sormak için
Bu e-Posta adresi istek dışı postalardan korunmaktadır, görüntülüyebilmek için JavaScript etkinleştirilmelidir