Terjedelmes bevezető után nézzünk egy egyszerű amőbaprogramot. A probléma nem igazán bonyolult, de pár dolgot azért tanulhatunk belőle:

  • Először is hogyan lehet amőbázásra bírni a számítógépet (ennek semmi köze a Visual C-hez, de érdekes).
  • Hogyan tudja egy ablak a saját méretét változtatni (akkora legyen az ablak, mint az amőbapálya beállított mérete).
  • Hogyan lehet egy ablak stílusát meghatározni (ne legyen rajta minimalizáló, maximalizáló gomb, ne legyen átméretezhető stb.).
  • Ezen kívül látni fogjuk, hogy egy osztálynak egynél több őse is lehet.

Kezdjünk először a program felépítésével.
 A legjobb, ha készítünk egy osztályt, amelyik tárolja, kezeli és létrehozza az amőbatáblát. Ennek az osztályunknak nem is lesz őse, és itt oldhatjuk meg a gép elleni játékot is. Lesz egy osztályunk az alkalmazásnak a CWinApp-ból származtatva, valamint egy osztály az ablaknak, aminek a CFrameWnd az őse.

Az amőbatáblát kezelő osztály létrehozása

Az eddig létrehozott osztályaink mind MFC osztályokból voltak örökítve, most viszont egy olyat akarunk hozzáadni a programunkhoz, ami nem MFC, sőt nincs is őse. Majdnem ugyanolyan egyszerűen is eljárhatunk, mint eddig, meg persze egy kicsit bonyolultabban is.
Először az egyszerűbb megoldás.
A főmenüben Insert\New Class… , vagy jobb egérgomb a ClassView-ban és new Class… . Ezt eddig is így csináltuk. Az itt megjelenő dialógusablakon állítsuk át a Class Type mezőt Generic Class-ra. Ekkor meg fog jelenni egy lista, ahová az ősöket tudjuk beírni, meg hogy milyen módon örökítünk belőle (ez szinte mindig public). Esetünkben a listát üresen hagyjuk, csak az új osztály nevét kell beírni, és készen vagyunk.
A némileg bonyolultabb megoldás.
A File\New… dialógusablakban létrehozunk egy fejléc (header) -és egy forrásfájlt az osztály számára, ügyelve arra, hogy az Add To Project ki legyen pipálva. Ekkor kaptunk két üres állományt. A fejlécállományban (valami.h) megadjuk az osztály deklarációját, a forrásállományban (valami.cpp) pedig megvalósítjuk a tagfüggvényeket. Arra kell vigyázni, hogy a cpp fájlban include-oljuk a stdafx.h-t , különben a fordítónak nem fog tetszeni a precompiled headerek (ezt nem tudtam lefordítani) miatt.

Ha ezzel megvagyunk, alakítsuk ki a class-unkat. Kell egy mutató, ami a pályát leíró mátrixra mutat. Minden eleme egy-egy byte, és vagy nulla (ekkor üres), vagy az ott lévő figura (kör vagy X) kódját tartalmazza. Tudnunk kell, melyik játékos jön, milyen jellel van a számítógép, és ki kezd. Ezeket is a figurákhoz tartozó kóddal tároljuk. Szintén rutinfeladat egy függvény elkészítése, ami végigpásztázza a pálya mátrixát, és jelzi, ha valaki nyert. És persze egy olyan sem árt, amivel a felhasználó lépni tud (a mátrixba ki kell csak írni az adott helyre a kódját).

Az igazi feladat a gép elleni játék megvalósítása.
Az itt leírtakon kívül nyílván létezik sokkal hatékonyabb megoldás is a probléma megoldására, ez csak az én megoldásom.

Abból az egyszerű ötletből induljunk ki, hogy ha jönne egy jótündér, és megmondaná, minden egyes pozícióra a pályán, hogy mennyire segít minket nyeréshez, akkor már könnyű dolgunk lenne. Csak meg kellene kérni: figyusz, nézd már meg az én szempontomból, hogy vannak ezek az értékek (továbbiakban hasznosság), meg az ellenfelem szempontjából.
Egy lépés annál jobb, minél fontosabb lett volna az ellenfelemnek az elfoglalt pozíció, és minél jobban segít engem a győzelemhez.
Tehát a tündérkétől kapott két értéket összeadjuk (vagy valamilyen más függvény szerint összegezzük) minden pozícióban, és oda lépünk, ahol a legnagyobb lett az összeg.
Ha egy olyan helyre léptünk, ami nekünk nem volt hasznos, viszont az ellenfél számára annyira hasznos volt, hogy a hasznosságok összege itt volt a legnagyobb a pályán, akkor védekeztünk.
Ha az ellenfél számára érdektelen helyre léptünk, akkor támadtunk, hiszen számunkra nyílván nagyon hasznos lehetett, ha az összeg ilyen nagy volt.
Tehát ha lenne egy jótündérünk, akkor már működne a dolog, védekezés is lenne, és támadás is. Próbáljuk meg helyettesíteni.

Egy pozíció akkor bír hasznossággal (segít a győzelemhez), ha vízszintesen, függőlegesen, vagy valamelyik átlóban rá lehet illeszteni egy olyan 5 (ötöt kell kirakni) egység hosszú szakaszt, ahol az ellenfélnek nincs jele, nekem viszont van. Ekkor van esélyem, hogy azon a szakaszon kirakjam az ötöt. Ha az ellenfélnek van a szakaszon valahol jele, akkor nekem azon az ötös szakaszon tuti nem lesz meg az öt, így számomra haszontalan, nem segít nyeréshez.

Összezavaró ábra:

A kékkel bekeretezett cellák az X számára hasznosak, a pirossal bekeretezettek pedig az O számára. Amelyik cellák nincsenek bekeretezve, az egyik fél számára sem bírnak hasznossággal, hiszen ott nem jöhet ki neki az 5. Bárki is következzen, biztos lehetünk benne, hogy egy olyan cellába fog rakni, ami pirossal és kékkel is be van keretezve, hiszen ezzel az ellenfélnek is keresztbetesz.
Ez a módszer akkor is bejön, ha nagyobb a tábla, ekkor tovább tudjuk csúsztatni az ötös kereteket:

Mindig egyenként csúsztatunk, amíg nem értük el a pálya végét. Látszik, hogy egy jel több ötösben is játszhat szerepet, mint itt a második O. A csúsztatás után kapott hasznosságokat hozzáadjuk egyszerűen a már meglévőekhez.
Arról is gondoskodnunk kell, hogy az a cella, ami két jelünk után jön, hasznosabbnak tűnjön, mint ami egy jelünk után jön. Ezt egy egyszerű táblázattal érjük el, amiben eltároljuk külön az ellenfél, és a mi szempontunkból, hogy mennyire hasznos adott számú jelet egymás mellé tenni. Nyilván sokkal hasznosabb egy négyest kialakítani, mint egy hármast. Az ellenfél hasznossági értékei legyenek egy hajszálnyit alacsonyabbak, hiszen hasznosabb kirakni egy ötöst, mint megakadályozni az ellenfélét.
A tömbök a programban:

static int HaszonNekem[5]={0,3,50,200,6000};
static int HaszonNeki[5]={0,2,49,199,5999};

Tehát az nem hasznos, ha a nincs sehol semmi, nekem 3 egységnyi hasznossággal bír egy hely, ha mellette minimum öttel van egy saját jelem, és abban az ötösben az ellenfélnek nincs jele. 50, ha két jelem van, 200, ha három, és 6000 (nagyon magas, mert ha odateszek, akkor nyertem), ha négy jelem van mellette.
Ezeknek az értékeknek a megváltoztatásával elérhető, hogy a program egész máshogy játsszon.

A hasznosságokat szintén egy mátrixban fogjuk tárolni. Vigyázzunk arra, hogy ha egy pozíció nem üres, akkor oda ne definiáljunk hasznosságot, ciki lenne odalépni.

Ezt a csúsztatásos dolgot horizontálisan, vertikálisan, a főátló és a mellékátló irányában eljátszva a hasznossági mátrix minden egyes szabad helynek a hasznosságát tartalmazni fogja.
 

Pl.: horizontálisan:
//a csúsztatás végig a táblán: … for (y=0; y<m_TableSize.cy; y++)   for (x=0; x<m_TableSize.cx-4; x++)    Haszon5H(x,y); … //x és y a kezdőpont void CAmoba::Haszon5H(int x, int y) { int Nekem=0,Neki=0,n,h=0,c; int ellen=EllenFel(); //mi az ellenfel jele //összeszámoljuk, kinek hány jele van abban az ötösben for(n=x;n<x+5;n++) {   c=TabAt(n,y); //az adott pozíción milyen jel van   if (c==m_Computer) Nekem++;           //ha a számítógépé akkor növeli a nekem számlálót   if(c==ellen) Neki++;           //ha az ellenfélé, akkor a Neki-t növeljük } if (Nekem==0) h=HaszonNeki[Neki];           //ha nekem nincs ott jelem, akkor neki hasznos if (Neki==0) h=HaszonNekem[Nekem];           //ha  neki nincs ott jele, akkor nekem hasznos for(n=x;n<x+5;n++) AddHaszon(n,y,h);           //mind az öt helyhez hozzáadjuk a hasznosságot }

Ezt négy irányba megcsináljuk, és meg is vannak a hasznosságok.
Ezek közül csak ki kell keresni a legnagyobbat, és odalépni.

Előfordulhat, hogy több ugyanolyan hasznosságú cella van, ekkor az emberi gondolkodáshoz hasonlóan választunk az ugyanolyan jónak tűnő lehetőségek közül. A véletlenre bízzuk.

A módszer messze nem tökéletes, de azért valahogy eldöcög. Azt is mondhatnám, hogy direkt ilyen, így legalább néha győzhetünk a gép ellen. De sajnos nem direkt.
Ennyit a játékstratégiáról.

A program összeállításában ott tartunk, hogy van egy osztályunk, amit a CFrameWnd-ből származtattunk. Ez végzi az ablak kirajzolását, kezelését. Az lenne az igazi, ha az imént megalkotott amőbázós osztály is az őse lenne, hiszen így rendelkezni fog annak minden tagjával, és szabadon használhatja őket. Ezt egyszerűen elérhetjük. Megkeressük a Frame osztályunk deklarációját, és az ősök közé egyszerűen beírjuk ezt az osztályt:

class CAmobaFrame : public CFrameWnd,CAmoba …

Ekkor az osztálynak több őse lesz, és mindegyik összes tulajdonságával rendelkezni fog. Tehát ablakot kezelni is tud, meg amőbázni is, és nekünk pont ez kell.

Az ablak kezeléséről és beállításáról

Azt szeretnénk, hogy az ablak pont akkora legyen, amin elfér a pálya, se nagyobb, se kevesebb. Ehhez több dolog is kell. Először az ablak már abban a méretben jelenjen meg, tehát bele kell szólnunk az ablak elkészítésébe. Erre való a PreCreateWindow virtuális függvény. Másodszor el kell érnünk, hogy a felhasználó ne tudja újraméretezni az ablakot, ha már olyan gondosan beállítottuk. Ezt szintén az előbb említett függvényben tehetjük meg. Harmadszor meg gondoskodni kell róla, ha menet közben változik a pálya mérete, az ablak újraméretezze magát.

Adjunk hozzá az osztályunkhoz egy PreCreateWindow virtuális függvényt. Vagy a ClassWizard-ból, vagy ClassView-nál jobb gomb és Add Virtual function….

Ebben a függvényben tudjuk megadni a létrehozandó ablak stílusát, és méretét. Ezt a paraméterként megkapott CREATESTRUCT típusú cs változó átírásával tehetjük meg. Számos beállítást módosíthatunk, most a méretet és a stílust adjuk meg. A méretet a cx és cy tagokba kell beírni, a stílust a style-ba. Sokféle stílus létezik, amire szükségünk lesz:
 

WS_BORDER Az ablaknak nem méretezhető kerete van.
WS_OVERLAPPED Szokványos keretes ablak
WS_SYSMENU Bezáró kis X megjelenik a jobb felső sarokban

Ennyi kell nekünk. A stílusokat OR kapcsolattal fűzhetjük össze.
Az ablak méretének meghatározásához szükségünk lesz a GetSystemMetrics Win32 API függvényre. Le lehet vele kérdezni a képernyő felbontását, az ablak címsorának magasságát, keretének szélességét pixelekben. A méret meghatározásához ezekre az adatokra lesz szükségünk.
 

SIZE CAmobaFrame::GetWinSize() { SIZE result; result.cx=m_Unit.cx*m_TableSize+2*m_Border.cx+              2*::GetSystemMetrics(SM_CXFRAME)+2; result.cy=m_Unit.cy*m_TableSize+2*m_Border.cy+              2*::GetSystemMetrics(SM_CYFRAME)+2   +::GetSystemMetrics(SM_CYMENU)   +::GetSystemMetrics(SM_CYCAPTION); return result; }

Ez a függvény adja vissza a létrehozandó ablak méretét. A pálya méretéből (m_TableSize) és a cella méretéből (m_Unit) megkapjuk a Client Area-t, ehhez még hozzá kell adni az ablak kereteit, címsorát, és menüjét. Tehát az ablak méretének és stílusának meghatározása, az a bizonyos PreCreateWindow:
 
 

BOOL CAmobaFrame::PreCreateWindow(CREATESTRUCT& cs) { SIZE winsize=GetWinSize(); //a fenti függvény megadja a méretet cs.cx=winsize.cx; //méret beállítása cs.cy=winsize.cy; cs.style=WS_OVERLAPPED|WS_BORDER|WS_SYSMENU; //stílus beállítása return CFrameWnd::PreCreateWindow(cs); }

Ahhoz, hogy az ablak saját magát tudja méretezni, szükségünk lesz a CWnd::GetWindowPlacement, és a CWnd::SetWindowPlacement függvényekre. Ezek segítségével tudjuk lekérdezni és beállítani az ablak elhelyezkedését. Mindkét függvény a WINDOWPLACEMENT típust használja. Segítségével megtudhatjuk, hogy az ablak milyen állapotban van (minimalizálva, maximalizálva, normál), és mekkora a mérete, mikor normál állapotban van. Nekünk erre az utolsóra lesz szükségünk.
 
 

void CAmobaFrame::ReSize() { WINDOWPLACEMENT wp; SIZE winsize=GetWinSize(); ::ZeroMemory(&wp,sizeof(wp)); wp.length=sizeof(wp); //a struktúra hosszára kel állítani GetWindowPlacement(&wp); wp.rcNormalPosition.right=wp.rcNormalPosition.left+winsize.cx;                        //a bal felső sarok marad wp.rcNormalPosition.bottom=wp.rcNormalPosition.top+winsize.cy; SetWindowPlacement(&wp); }

A mellékelt forráskód: Forráskód.zip (94 KB)
Amoba lefordítva (EXE): Abôba.exe (166 KB)

Ennyi.