Életjáték

Használható segédanyagok: Haskell könyvtárainak dokumentációja, (lokális!) Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók. Ha bármilyen kérdés, észrevétel felmerül, azt a felügyelőknek kell jelezni, nem a diáktársaknak!

A feladatok egymásra épülnek ezért érdemes ezeket a megadásuk sorrendjében megoldani, de legalább megérteni az aktuális feladatot megelőző feladatokat! A függvények definíciójában lehet, sőt javasolt is alkalmazni a korábban definiált függvényeket (függetlenül attól, hogy sikerült-e azokat megadni).

Tekintve, hogy a tesztesetek, bár odafigyelés mellett íródnak, nem fedik le minden esetben a függvény teljes működését, határozottan javasolt még külön próbálgatni a megoldásokat beadás előtt, vagy megkérdezni a felügyelőket!

Részpontszámokat csak az elégséges szint elérése után lehet kapni!

A feladat összefoglaló leírása

Az életjáték (,,the game of life’’) egy speciális sejtautomata, amit John Conway angol matematikus fejlesztett ki 1970-ben. Rengeteg különböző változata létezik, a következőkben leírt variáns a klasszikusnak számító változat.

Az életjáték végtelen kétdimenziós térben, egy négyzetrácson játszódik. A négyzetrácsot cellák alkotják. Minden cellában maximum egy élő sejt lehet. Gyakori ábrázolási mód, hogy az üres cellákat fehér, míg az élő sejtet tartalmazó cellákat fekete négyzetek jelölik, így egy kép rajzolható ki az aktuális állapotból.

Valójában ez a játék nem igényli játékos aktív beavatkozását, csupán a kezdeti pillanatban élő sejtek helye adható meg. Ez után minden körben kiszámítunk egy új generációt, tehát azoknak a celláknak a helyét, ahol a lépés után élő sejtek lesznek. Három eset lehetséges: egy sejt életben maradhat a következő generációra, meghalhat, vagy új sejt születhet egy üres cellában. Ezt a következő szabályok határozzák meg. (Forrás: Wikipedia)

  1. A sejt túléli a kört, ha két vagy három szomszédja van.
  2. A sejt elpusztul, ha kettőnél kevesebb, vagy háromnál több szomszédja van. Az előbbit elszigetelődésnek, az utóbbit pedig túlnépesedésnek nevezzük.
  3. Új sejt születik minden olyan cellában, melynek környezetében pontosan három sejt található.

Egy cella környezete a hozzá legközelebb eső 8 mező (tehát a cellához képest átlósan elhelyezkedő cellákat is figyelembe vesszük, és feltesszük, hogy a négyzetrácsnak nincs széle).

Minden cellát a koordinátái írnak le. A koordináta rendszer x tengelye jobbra növekszik, az y tengelye pedig lefelé. Mivel a tér végtelen, negatív koordináták is megengedettek, de csak egész értékekkel dolgozunk.

Ügyeljünk arra, hogy a koordináták előbb az y, majd az x tengelyen vett pozíciót tartalmazzák, a számítógépes grafikában megszokott módon!

type Coordinate = (Integer, Integer)

A játék egy állapota az aktuális generáció (az élő sejtek) celláinak megadásából áll:

type Generation = [Coordinate]

Egy lépés során tehát az a dolgunk, hogy az aktuális generációból kiszámítsuk az új generációt a fenti szabályok alapján. Ez minden cellára szimultán történik, tehát az új generáció kiszámítása során csak az előző generációt vesszük figyelembe, a már éppen megszülető vagy elpusztuló sejteket nem!

Példák

(Az ábrák a conwaylife.com honlapról származnak.)

Egyetlen sejt, amely a következő generációra elszigetelődés miatt kihal:

single :: Generation
single = [ (42, 42) ]

Blokk, ahol mindig ugyan ezek a sejtek maradnak életben, és nem születnek újak sem:

block :: Generation
block = [ (5, 6), (5, 7)
        , (6, 6), (6, 7) ]
Block
Block

Kétlépéses oszcillátor, ahol az egyik folyton a másikba alakul egy lépés után. Valójában így egy alakzatnak számítanak, ennek a neve blinker.

row :: Generation
row = [ (10, 1), (10, 2), (10, 3) ]
Blinker row
Blinker row
column :: Generation
column = [ (9,  2)
         , (10, 2)
         , (11, 2) ]
Blinker column
Blinker column

Háromlépéses oszcillátor, amely önmagába tér vissza három lépés után:

caterer :: Generation
caterer = [ (2, 4), (3, 2), (3, 6), (3, 7), (3, 8), (3, 9)
          , (4, 2), (4, 6), (5, 2), (6, 5), (7, 3), (7, 4) ]
Caterer
Caterer

Szomszédos cellák (1 pont)

Készítsünk egy függvényt, amely egy cella koordinátái alapján megadja a vele szomszédos cellák koordinátáit! Mivel a tér végtelen, minden cellának pontosan 8 szomszédja van. A szomszédokat olyan sorrendben adjuk meg, hogy sorokban fentről lefelé, ezen belül pedig balról jobbra legyenek felsorolva.

neighbors :: Coordinate -> [Coordinate]

Test>
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1), (2, 2)] :: [Coordinate]
Test>
[(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] :: [Coordinate]
Test>
[(99, -201), (99, -200), (99, -199), (100, -201), (100, -199), (101, -201), (101, -200), (101, -199)] :: [Coordinate]

Szomszédos élő sejtek (1 pont)

Határozzuk meg egy generáció és egy cella alapján, hogy egy adott cellának hány élő sejtet tartalmazó szomszédja van!

livingNeighbors :: Generation -> Coordinate -> Int

Test>
0 :: Int
Test>
0 :: Int
Test>
1 :: Int
Test>
1 :: Int
Test>
2 :: Int
Test>
3 :: Int

Szabályok kiszámítása (2 pont)

Határozzuk megy egy cellához az aktuális generáció segítségével, hogy élő szomszédai száma és a fenti szabályok alapján élő sejtet fog-e tartalmazni következő generációban! Természetesen ehhez azt is figyelembe kell venni, hogy az adott generációban van-e élő sejt!

staysAlive :: Generation -> Coordinate -> Bool

Test>
False :: Bool
Test>
False :: Bool
Test>
True :: Bool
Test>
True :: Bool
Test>
False :: Bool
Test>
True :: Bool
Test>
True :: Bool

Szabályok alkalmazása élő sejtekre (1 pont)

Alkalmazzuk a fenti szabályokat az aktuális generációra! Mivel egy generáció csak az élő sejtek celláinak koordinátáit tárolja, ezért két dolog történhet minden cellával: a benne lévő sejt vagy életben marad, vagy meghal.

stepLivingCells :: Generation -> Generation

Test>
[] :: Generation
Test>
[(10, 2)] :: Generation
Test>
[(10, 2)] :: Generation

Élő sejtekkel szomszédos üres cellák (1 pont)

Számítsuk ki azokat az üres cellákat, amelyek az adott generáció élő sejtjeinek szomszédai! Ügyeljünk rá, hogy minden cella csak egyszer szerepeljen!

deadNeighbors :: Generation -> [Coordinate]

Test>
[(41, 41), (41, 42), (41, 43), (42, 41), (42, 43), (43, 41), (43, 42), (43, 43)] :: [Coordinate]
Test>
[(9, 0), (9, 1), (9, 2), (10, 0), (11, 0), (11, 1), (11, 2), (9, 3), (11, 3), (9, 4), (10, 4), (11, 4)] :: [Coordinate]
Test>
[(8, 1), (8, 2), (8, 3), (9, 1), (9, 3), (10, 1), (10, 3), (11, 1), (11, 3), (12, 1), (12, 2), (12, 3)] :: [Coordinate]

Szabály alkalmazása üres cellákra (1 pont)

Alkalmazzuk a fenti szabályokat az aktuális generáció élő sejtjei körüli üres cellákra! Természetesen ezeken a helyeken csak újabb sejtek születhetnek.

stepDeadCells :: Generation -> Generation

Test>
[] :: Generation
Test>
[(9, 2), (11, 2)] :: Generation
Test>
[(10, 1), (10, 3)] :: Generation

Új generáció kiszámítása (2 pont)

Számítsuk ki a következő generációt az aktuális generációból! Ehhez a következő lépéseket kell elvégezni:

stepCells :: Generation -> Generation

Test>
[] :: Generation
Test>
[(9, 2), (10, 2), (11, 2)] :: Generation
Test>
True :: Bool
Test>
[(10, 1), (10, 2), (10, 3)] :: Generation
Test>
True :: Bool

Játék léptetése a megadott számú generációba (1 pont)

Készítsük el azt a függvényt, amely egy kezdeti generációra elvégzi az adott számú lépést! A kezdeti generációt vegyük a nulladiknak. Ha a megadott generáció száma negatív, a lenti példában található hibát váltsuk ki!

play :: Generation -> Int -> Generation

Test>
[(42, 42)] :: Generation
Test>
[] :: Generation
Test>
[] :: Generation
Test>
True :: Bool
Test>
True :: Bool
Test>
True :: Bool
Test>
True :: Bool
Test>
⊥₁ :: Generation
⊥₁: play: generation number must be non-negative.
CallStack (from HasCallStack):
  error, called at ./GameOfLife.lhs:258:14 in main:GameOfLife

Stabil generációk detektálása (1 pont)

Állapítsuk meg egy generációról, hogy stabil-e, azaz a léptetése után változatlan marad!

isStill :: Generation -> Bool

Test>
False :: Bool
Test>
True :: Bool
Test>
False :: Bool

Oszcillátorok detektálása (3 pont)

Állapítsuk meg egy generációról, hogy oszcillátor-e, azaz visszatér-e önmagába egy megadott lépésnyi távolságon belül. Ha igen, adjuk meg azt a legkisebb pozitív számot, ahány generáció múlva ez megtörténik. Ha nem, adjuk a lenti példában megadott hibajelzést.

isOscillator :: Generation -> Int -> Int

Test>
2 :: Int
Test>
2 :: Int
Test>
3 :: Int
Test>
3 :: Int
Test>
1 :: Int
Test>
⊥₁ :: Int
⊥₁: isOscillator: not an oscillator with the given limit
CallStack (from HasCallStack):
  error, called at ./GameOfLife.lhs:296:18 in main:GameOfLife
Test>
⊥₁ :: Int
⊥₁: isOscillator: not an oscillator with the given limit
CallStack (from HasCallStack):
  error, called at ./GameOfLife.lhs:296:18 in main:GameOfLife
Test>
⊥₁ :: Int
⊥₁: isOscillator: not an oscillator with the given limit
CallStack (from HasCallStack):
  error, called at ./GameOfLife.lhs:296:18 in main:GameOfLife

Pontozás