25. Programozási feladatok - Életjáték

Az életjáték leírása itt található a Wikipédián, de azért a játék lényegét most leírom:

Képzeljünk el egy végtelen kiterjedésű "kockás" papírt. Rajzoljunk be a négyzetekbe pöttyöket. Ezek lesznek a sejtek. A sejtek az új generációja úgy jön létre, ha van a környezetükben másik sejt, de nincsen túl sok! Ez azt jelenti, hogyha 2 vagy 3 szomszédos helyen van sejt, akkor az aktuális helyen lévő sejt tovább él. Ha kevesebb van, akkor "éhenhal", ha több szomszédban van sejt, akkor megfojtják.

Készíts olyan programot, amellyel feltöltheted a játékteret és amely a generációkat lépteti.A program lehet konzolos, akkor az egyes helyek a konzol képernyőhelyeit jelentik. Ha grafikus felületet akarsz használni, akkor célszerűen checkboxok legyenek az egyes helyek.

Megoldási gondolatok 1.

Az adatok tárolásához egy NxN-es tömböt kell deklarálnunk, ahol N egy pozitív egész szám. Mivel végtelen területet nem tudunk létrehozni, eért N legyen elég nagy szám (tezsteléshez elég 10-20 között)!

Végig kell néznünk a játékteret (két darab egymásba ágyazott ciklussal) és meg kell néznünk minden egyes hely szomszédainak a számát. Ha 2 vagy 3 szomszédon van akkor a következő generációban az adott helyen sejt lesz. Nyolc szomszéd lehet.

//  8 szomszéd hely
//  o o o
//  o X o
//  o o o

for (int i=0;i<N;i++){
    for(int j=0; j<N ; j++){
        db = 0;
        try{
            if( t[i-1,j-1] >0 ) db++;
        }catch{}
        ....     //Itt nyolc helyet kell megvizsgálni
        ....
        if( db >1 && db<4) 
           kov[i,j] =1;
        else
           kov[i,j] = 0;
    }
}

Megjegyzés

​A try{}catch{} szerkezet azért fontos, mert a tömbindexek a vizsgálatnál kívül eshetnek a tömbhatárokon. Ezt persze lehetne orvosolni úgy, hogy a program nyelv 0-tól indexeli a tömböket N-1-ig, de mi 0-tó N+2-ig deklaráljuk a tömböt és akkor néhány elemet nem használunk semmire, de nem kell használnunk a hibakezeléss!

A programrészlet lefutása után a kov[] tömbben lesznek a következő generáció adata, amelyet ki kel vissza kell másolnunk az eredeti tömbbe. Ehhez is két egymásba ágyazott ciklusra van szükségünk.

for (int i=0; i < N;i++){
    for(int j=0; j < N ; j++){
        t[i,j] = kov[ i, j ];
    }
}

Megjegyzés

Amikor egy program egymás utáni állapotokat jelenít meg olyan módon, hogy az előző állapot adataiból (minden adatából) kell a következő állapot adatait kiszámolnunk, akkor mindig két adattáblát kell használnunk, mivel ha a változásokat az aktuális táblába írnánk, akkor rögtön elrontanánk a tartalmát. A megoldás először az új kiszámolt adatokat egy új táblázatba tesszük, majd a végén az egészet visszamásoljuk az eredeti adattáblába.

Az adatok beviteléről és megjelenítéséről

Nyilvánvalóan a megjelenítést is két egymásba ágyazott ciklussal végezhetjük és egy i,j koordinátájú helyre kiteszünk egy pöttyöt, ha van élet t[i,j] >0 a feltétel és nem teszünk semmit, ha nincsen t[i,j] == 0;

Ha konzolos alkalmazást írunk, akkor célszerűen a kezdőadatokat kézzel kell bevinnünk, vagy fájlból beolvasnunk. Ha fájlból olvasunk be, akkor a fájlban soronként tárolnunk érdemes annyi 0 vagy 1 értéket ;-ve elválasztva, ahány oszlopunk van. A fájlban pedig annyi sornak kell lennie, ahány soros a táblázat. Az adatok kijelzéséhez pedig használhatjuk:

for (int i=0; i < N;i++){
    for(int j=0; j < N ; j++){
        Console.SetCursorPosition(i,j);
        if(t[i,j]>1){
           Console.Write("x");
        }else{
           Console.Write(" ");
        }
    }
}

Ha Windows Form Applicationt használunk, akkor célszerű CheckBoxokat létrehozni és az adatok értéke alapján a CheckBox.CheckState = CheckState.Checked; CheckBox.CheckState = CheckState.Unchecked állapotot beírni az aktuális checkboxba;

for (int i=0; i < N;i++){
    for(int j=0; j < N ; j++){       
        if(t[i,j]>1){
            this.checks[i,j].CheckState=CheckState.Checked;
        }else{
            this.checks[i,j].CheckState=CheckState.Unchecked
        }
    }
}

Másik megoldási gondolatok

A programban kritikus az a rész, amikor egy pont környzetében megnézzük, hogy hány másik pont létezik. Azért kritikuis, mert a 0. és az utolsó (N-1-ik) sorban és oszlopban is vizsgálni kellene az eggyel nagyobb és kisebb helyeket, ami ugye már a tömbön kívül helyet jelentenek. Az előző példában erre való volt a try...catch szerkezet. A másik megoldás azonban elegánsabb és gyorsabb is általában egy kis plusz memóriafoglalás árán.

A játékteret ne 0....N-1 között határozzuk meg, hanem 1...N között, továbbá a legkisebb sorú és oszlopú elem előtt és a legnagyobb sorú és oszlopú sor után is legyen egy-egy plusz sor és oszlop. Vagyis a tömbünk legyen t[N,N] legyen hanem t[N+2, N+2] és a játéktér 1..N-ig tartson!

Ettől kezdve minden tömbművelet úgy néz ki, hogy:

for (int i=1; i < N+1;i++){
    for(int j=1; j < N+1 ; j++){       
      ..... tetszőleges műveletek....
    }
}

Ez után az sem lesz probléma, ha a játéktér szélső vagy utolsó sorának a környezetét nézzük meg, mert lesz a tömbnek ilyen oszlopa és sora, nem akad ki a program. Igaz, hogy az ezekben lévő értékek 0-ák lesznek, de az nekünk pont jó lesz. Ezeket a helyeket nem kell megjelenítenünk, mert úgyis nulla az értéke, vagyis nincsn élő sejt bennük!

Ha a windows Form Applicationt használunk, akkor a korábban említett két darab két dimenziós tömb közül az egyik lehet a checkboxok tömbje és csak a következő generáció legyen egy "mezei" integerekből álló tömb. ebben az esetben az alábbi módon lehet megnézni, hogy mi lesz a következő generációban

int db = 0;

for(int i = 1; i < N+1 ; i++)
{
    for( int j = 1; j < N+1 ; j++)
    {
        //Megnézzük az aktuális pont körüli pontok számát
        db = 0;
        for(int k=i-1;k <= i+1; k++)
        {
            for(int l=j-1; l <=j+1; l++)
            {
                //Ha az adott checkbox be van jelölve, akkor növelem a db-ot eggyel.
                db += this.checks[k, l].CheckState == CheckState.Checked ? 1:0;
            }
        }
        //Ha az aktuális pont be van jelölve, akkor csökkenteni kell a db értékét 1-gyel.
        if (this.checks[i, j].CheckState == CheckState.Checked ) db--;
                    
        //Ha a pontnak 2 vagy 3 szomszédja van, akkor a következő generációban él.
        if ((2 == db ) || (db == 3))
            kovgeneracio[i, j] = 1;
        else
            kovgeneracio[i, j] = 0;                    
    }
}

//A következő generáció szerint beállítom acheckboxokat
for (int i = 1; i < N+1; i++)
{
    for (int j = 1; j < N+1 ; j++)
    {
        if (kovgeneracio[i, j] == 1)
            this.checks[i, j].CheckState = CheckState.Checked;
        else
            this.checks[i, j].CheckState = CheckState.Unchecked;
        }
    }   
}

Sajnos konzolos alkalmazás esetén mindenképpen kell a két tömb!