NxN-es Tic Tac Toe já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

A Tic Tac Toe egy kétszemélyes játék egy n × n-es táblán, ahol két játékos felváltva lép. Minden lépésben a játékos a saját jelét ('O' vagy 'X') helyezi el a tábla szabad mezőinek egyikén. Az a játékos nyeri a játékot, akinek előbb sikerül egy teljes sort, oszlopot vagy átlót kitöltenie a saját jelével. A játék során arra törekszünk, hogy minél előbb tudjuk teljesíteni a nyeréshez szükséges feltételt, de az ellenfél játékost is akadályozni kell a játék során, nehogy Ő nyerjen.

Vizuálisan (az illusztráció a Wikipediáról származik):

Tic_Tac_Toe
Tic_Tac_Toe

A feladatban a játékhoz szükséges reprezentációt fogjuk megadni, illetve a játék játszásához és lehetséges kimenetek vizsgálatához szükséges műveleteket definiáljuk.

A játékban használt tábla mezőit (sor, oszlop) párokkal (Cell) reprezentáljuk. A sor- és oszlopindexek 0 és n - 1 közötti értékeket vesznek fel. A játékban szereplő táblát egy n2 méretű listával reprezentáljuk (Grid), amelyben sorfolytonosan szerepelnek a tábla különböző pozíciói:

type Cell = (Int, Int)
type Grid = [Cell]

A tábla mezőinek a koordinátái (1 pont)

Adjuk meg azt a függvényt, amely egy adott méretű táblának sorfolytonosan megadja az összes lehetséges pozícióját!

cells :: Int -> Grid

Test>
[] :: Grid
Test>
[(0, 0), (0, 1), (1, 0), (1, 1)] :: Grid
Test>
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4)] :: Grid
Test>
[] :: Grid

A tábla sorai (1 pont)

Adjuk meg azt a függvényt, amely egy adott méretű táblának megadja az összes sorát! A függvény eredménye soronként csoportosítva tartalmazza a koordinátákat!

rows :: Int -> [[Cell]]

Test>
[] :: [[Cell]]
Test>
[[(0, 0)]] :: [[Cell]]
Test>
[[(0, 0), (0, 1), (0, 2), (0, 3)], [(1, 0), (1, 1), (1, 2), (1, 3)], [(2, 0), (2, 1), (2, 2), (2, 3)], [(3, 0), (3, 1), (3, 2), (3, 3)]] :: [[Cell]]
Test>
[[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)], [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4)], [(2, 0), (2, 1), (2, 2), (2, 3), (2, 4)], [(3, 0), (3, 1), (3, 2), (3, 3), (3, 4)], [(4, 0), (4, 1), (4, 2), (4, 3), (4, 4)]] :: [[Cell]]
Test>
[] :: [[Cell]]

A tábla oszlopai (1 pont)

Adjuk meg azt a függvényt, amely egy adott méretű táblának megadja az összes oszlopát! A függvény eredménye oszloponként csoportosítva tartalmazza a koordinátákat!

cols :: Int -> [[Cell]]

Test>
[] :: [[Cell]]
Test>
[[(0, 0)]] :: [[Cell]]
Test>
[[(0, 0), (1, 0), (2, 0), (3, 0)], [(0, 1), (1, 1), (2, 1), (3, 1)], [(0, 2), (1, 2), (2, 2), (3, 2)], [(0, 3), (1, 3), (2, 3), (3, 3)]] :: [[Cell]]
Test>
[[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)], [(0, 1), (1, 1), (2, 1), (3, 1), (4, 1)], [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2)], [(0, 3), (1, 3), (2, 3), (3, 3), (4, 3)], [(0, 4), (1, 4), (2, 4), (3, 4), (4, 4)]] :: [[Cell]]
Test>
[] :: [[Cell]]

A tábla átlói (1 pont)

Adjuk meg azt a függvényt, amely egy adott méretű tábla két átlóját adja meg! Az eredményben előbb a főátló koordinátáinak listája szerepel, majd a mellékátló koordinátáinak listája.

A főátló a első sor első elemétől az utolsó sor utolsó eleméig, a mellékátló pedig az első sor utolsó elemétől az utolsó sor első eleméig tart.

Például (a főátló vastaggal, míg a mellékátló dőlttel szedve):

[(0, 0), (0, 1), (0, 2), (0, 3),
(1, 0), (1, 1), (1, 2), (1, 3),
(2, 0), (2, 1), (2, 2), (2, 3),
(3, 0), (3, 1), (3, 2), (3, 3)]
diags :: Int -> [[Cell]]

Test>
[[], []] :: [[Cell]]
Test>
[[(0, 0)], [(0, 0)]] :: [[Cell]]
Test>
[[(0, 0), (1, 1), (2, 2), (3, 3)], [(0, 3), (1, 2), (2, 1), (3, 0)]] :: [[Cell]]
Test>
[[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)], [(0, 4), (1, 3), (2, 2), (3, 1), (4, 0)]] :: [[Cell]]
Test>
[[], []] :: [[Cell]]

Bevezetjük a játékosok jelölésére a Player szinonimát. A játékosokat a nekik megfelelő 'o', 'O', 'x' és 'X' karakterekkel fogjuk azonosítani.

type Player = Char

Játékos validálása (1 pont)

Készítsünk egy olyan függvényt, amely csak akkor alkalmazza a paraméterül kapott függvényt a paraméterül kapott játékosra, ha a játékosnak megfelelő jelek valamelyikét kapta! Ellenkező esetben használjuk az error függvényt (a példában látható módon)!

validatePlayer :: (Player -> a) -> Player -> a

Test>
'o' :: Char
Test>
'X' :: Char
Test>
'X' :: Char
Test>
'O' :: Char
Test>
⊥₁ :: Char
⊥₁: validatePlayer: illegal player 'i'
CallStack (from HasCallStack):
  error, called at ./NxNTicTacToe.lhs:162:25 in main:NxNTicTacToe

Ellenfél meghatározása (1 pont)

Definiáljuk azt a függvényt, amely a paraméterül kapott játékosnak megadja az ellenfele jelét, amennyiben megfelelő a jel! A függvény minden esetben nagybetűként adja vissza a játékos jelét!

otherPlayer :: Player -> Player

Test>
'X' :: Player
Test>
'X' :: Player
Test>
'O' :: Player
Test>
'O' :: Player
Test>
⊥₁ :: Player
⊥₁: validatePlayer: illegal player 'z'
CallStack (from HasCallStack):
  error, called at ./NxNTicTacToe.lhs:162:25 in main:NxNTicTacToe
Test>
⊥₁ :: Player
⊥₁: validatePlayer: illegal player '.'
CallStack (from HasCallStack):
  error, called at ./NxNTicTacToe.lhs:162:25 in main:NxNTicTacToe

A játékállás jelölésére bevezetünk egy állapotot (State), amelyet a játék aktuális állapotának leírására használjuk a későbbiekben. Az állapot egy rendezett hármas, amely felépítése a következő:

type State = ([Cell], [Cell], Int)

A kezdeti állapot (1 pont)

Adjuk meg azt a függvényt, amely a kiinduló állapotot adja vissza! A függvény argumentumként megkapja a játék során használt tábla méretét.

initState :: Int -> State

Test>
([], [], 1) :: State
Test>
([], [], 1) :: State

Megadott mező ellenőrzése (1 pont)

Definiáljuk azt a műveletet, amely paraméterül kap egy egész számot (n) és egy koordinátát, majd eldönti, hogy az adott koordináta szerepelhet-e egy n × n méretű táblán! A függvény adjon vissza igazat, ha megfelelő a koordináta!

isValidCell :: Int -> Cell -> Bool

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

Szabad-e a mező? (1 pont)

Készítsük el azt a függvényt, amely megvizsgálja, hogy az adott koordináta szabad-e! Egy koordinátát szabadnak tekintünk, ha egyik játékos sem helyezett el jelet az adott koordinátán.

isCellFree :: State -> Cell -> Bool

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

Megengedett-e a lépés? (1 pont)

Adjuk meg azt a függvényt, amely megvizsgálja, hogy szabad-e arra a koordinátára jelet helyezni! Akkor szabad egy pozícióra jelet helyezni, ha az a koordináta megfelel a tábla méretének és még egyik játékos jele sincs az adott pozíción.

isValidStep :: State -> Cell -> Bool

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

Egy lépés végrehajtása (1 pont)

Adjuk meg azt a függvényt, amely a megadott állapotot frissíti és elhelyezi a kért koordinátára a megadott játékos jelét! Ügyeljünk arra, hogy csak akkor szabad az állapotot frissíteni, ha a lépés az adott állapotból megengedett! Amennyiben nem megfelelő lépést kellene végrehajtani, akkor használjuk az error függvényt a tesztesetek szerinti üzenettel!

step :: Player -> State -> Cell -> State

Test>
([(1, 1)], [], 4) :: State
Test>
([(1, 1)], [(1, 2)], 4) :: State
Test>
⊥₁ :: State
⊥₁: step: invalid move (1,1) of player 'X'
CallStack (from HasCallStack):
  error, called at ./NxNTicTacToe.lhs:277:32 in main:NxNTicTacToe
Test>
⊥₁ :: State
⊥₁: step: invalid move (1,1) of player 'O'
CallStack (from HasCallStack):
  error, called at ./NxNTicTacToe.lhs:277:32 in main:NxNTicTacToe

Szabadon álló mezők pozíciói (1 pont)

Adjuk meg azokat a pozíciókat, amelyek az állapotban jelzett n × n táblából még szabadon állnak!

freeCells :: State -> [Cell]

Test>
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4)] :: [Cell]
Test>
[(0, 0), (0, 2), (0, 3), (0, 4), (1, 0), (1, 1), (1, 3), (1, 4), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (3, 0), (3, 1), (3, 3), (3, 4), (4, 1), (4, 2), (4, 3), (4, 4)] :: [Cell]

Összes lehetséges lépés egy állapotból (2 pont)

Adjuk meg azt a függvényt, amely egy adott állapotból a rendelkezésre álló (vagyis szabad) mező felhasználásával előállítja az összes lehetséges lépést!

steps :: Player -> State -> [State]

Test>
[([(0, 0)], [], 3), ([(0, 1)], [], 3), ([(0, 2)], [], 3), ([(1, 0)], [], 3), ([(1, 1)], [], 3), ([(1, 2)], [], 3), ([(2, 0)], [], 3), ([(2, 1)], [], 3), ([(2, 2)], [], 3)] :: [State]
Test>
[([(0, 0)], [(0, 1)], 3), ([(0, 0)], [(0, 2)], 3), ([(0, 0)], [(1, 0)], 3), ([(0, 0)], [(1, 1)], 3), ([(0, 0)], [(1, 2)], 3), ([(0, 0)], [(2, 0)], 3), ([(0, 0)], [(2, 1)], 3), ([(0, 0)], [(2, 2)], 3), ([(0, 1)], [(0, 0)], 3), ([(0, 1)], [(0, 2)], 3), ([(0, 1)], [(1, 0)], 3), ([(0, 1)], [(1, 1)], 3), ([(0, 1)], [(1, 2)], 3), ([(0, 1)], [(2, 0)], 3), ([(0, 1)], [(2, 1)], 3), ([(0, 1)], [(2, 2)], 3), ([(0, 2)], [(0, 0)], 3), ([(0, 2)], [(0, 1)], 3), ([(0, 2)], [(1, 0)], 3), ([(…, …)], [(…, …)], 3), ([…, ……], […, ……], 3), (…, …, …), …, ……] :: [State]
Test>
[([(1, 1), (0, 0), (0, 1)], [(0, 2), (2, 0), (2, 1)], 3), ([(1, 1), (0, 0), (0, 1)], [(1, 0), (2, 0), (2, 1)], 3), ([(1, 1), (0, 0), (0, 1)], [(1, 2), (2, 0), (2, 1)], 3), ([(1, 1), (0, 0), (0, 1)], [(2, 2), (2, 0), (2, 1)], 3)] :: [State]

Nyert-e az állapot? (2 pont)

Adjuk meg azt a függvényt, amely megvizsgálja, hogy a paraméterül kapott játékosnak van-e nyerő állása a megadott állapotban! A játékos akkor nyer, ha a lépései között (tetszőleges sorrendben) megtalálható a tábla valamelyik teljes sora, oszlopa vagy átlója!

Segítség: A megoldáshoz segítséget nyújthat, ha felhasználjuk a korábban definiált rows, cols és diags függvényeket, hiszen a függvények által visszaadott listák valamelyikének minden eleme megtalálható az adott játékos lépési között, akkor az a játékos megnyerte a játékot.

won :: Player -> State -> Bool

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

Állapotok transzformálása (3 pont)

A későbbiekben szeretnénk az összes lehetséges lejátszási lehetőséget előállítani, ezért szükségünk lesz némi előkészületre. Ha egy táblát az összes lehetséges módon szeretnénk kitölteni (betartva, hogy a játékosok felváltva léphetnek), már egy 3 × 3-as tábla esetén hatalmas számról beszélünk, hiszen ez 9! (azaz 362880).

Az előbb megadott szám azt jelenti, hogy a példában szereplő 9 mezőt hányféleképpen lehet kitölteni. De beláthatjuk, hogy egy-egy játék esetén sokkal korábban hirdethető nyertes, minthogy az összes mezőt ki kelljen tölteni! Ha ezeket az állapotokat kigyűjtjük és nem folytatjuk belőlük a játékot, akkor csökkenteni tudunk a játékok számát (esetünkben kb. 42%-kal csökken), de ez még így is tetemes mennyiségű (209088 játék).

Adjuk meg azt a függvényt, amely paraméterként megkap egy rendezett hármast, melynek elemei:

A függvény az alábbiak szerint működik:

A függvény eredménye egy rendezett hármas lesz, amely elemei:

fork :: (Player, [State], [State]) -> (Player, [State], [State])

Test>
('X', [([(0, 0)], [], 3), ([(0, 1)], [], 3), ([(0, 2)], [], 3), ([(1, 0)], [], 3), ([(1, 1)], [], 3), ([(1, 2)], [], 3), ([(2, 0)], [], 3), ([(2, 1)], [], 3), ([(2, 2)], [], 3)], []) :: (Player, [State], [State])

Hányféle lejátszás létezik? (2 pont)

Definiáljuk azt a függvényt, amelyik paraméterként megkapja a soron következő játékos jelét, illetve az eddig elért játszmák listáját és lejátssza ezeket a fork művelet segítségével! A függvény eredménye az adott játszmákat reprezentáló állapotok listája!

Segítség: A függvény csak akkor ad eredményt, ha minden játszmában kiderült a nyertes!

play :: Player -> [State] -> [State]

Test>
[([(1, 0), (0, 0)], [(0, 1)], 2), ([(1, 1), (0, 0)], [(0, 1)], 2), ([(0, 1), (0, 0)], [(1, 0)], 2), ([(1, 1), (0, 0)], [(1, 0)], 2), ([(0, 1), (0, 0)], [(1, 1)], 2), ([(1, 0), (0, 0)], [(1, 1)], 2), ([(1, 0), (0, 1)], [(0, 0)], 2), ([(1, 1), (0, 1)], [(0, 0)], 2), ([(0, 0), (0, 1)], [(1, 0)], 2), ([(1, 1), (0, 1)], [(1, 0)], 2), ([(0, 0), (0, 1)], [(1, 1)], 2), ([(1, 0), (0, 1)], [(1, 1)], 2), ([(0, 1), (1, 0)], [(0, 0)], 2), ([(1, 1), (1, 0)], [(0, 0)], 2), ([(0, 0), (…, …)], [(0, 1)], 2), ([(…, …), …, ……], [(…, …)], 2), ([…, ……], […, ……], 2), (…, …, …), …, ……] :: [State]
Test>
[([(0, 0)], [], 1)] :: [State]
Test>
[([(2, 2), (1, 2), (1, 0), (0, 1), (0, 2)], [(1, 1), (0, 0), (2, 0), (2, 1)], 3), ([(1, 2), (2, 2), (1, 0), (0, 1), (0, 2)], [(1, 1), (0, 0), (2, 0), (2, 1)], 3), ([(2, 2), (1, 2), (1, 0), (0, 1), (0, 2)], [(0, 0), (1, 1), (2, 0), (2, 1)], 3), ([(1, 2), (2, 2), (1, 0), (0, 1), (0, 2)], [(0, 0), (1, 1), (2, 0), (2, 1)], 3), ([(0, 0), (2, 2), (1, 0), (0, 1), (…, …)], [(1, 2), (1, 1), (2, 0), (2, 1)], 3), ([(0, 0), (2, 2), (1, 0), (…, …), …, ……], [(1, 1), (1, 2), (2, 0), (…, …)], 3), ([(0, 0), (2, 2), (…, …), …, ……], [(1, 2), (1, 0), (…, …), …, ……], 3), ([(0, 0), (…, …), …, ……], [(1, 0), (…, …), …, ……], 3), ([(…, …), …, ……], [(…, …), …, ……], 3), ([…, ……], […, ……], 3), (…, …, …), …, ……] :: [State]
Test>
[([(0, 0), (0, 1), (0, 2)], [(2, 0), (2, 1)], 3)] :: [State]
Test>
[([(2, 2), (1, 2), (1, 0), (0, 1), (0, 2)], [(1, 1), (0, 0), (2, 0), (2, 1)], 3), ([(1, 2), (2, 2), (1, 0), (0, 1), (0, 2)], [(1, 1), (0, 0), (2, 0), (2, 1)], 3), ([(2, 2), (1, 2), (1, 0), (0, 1), (0, 2)], [(0, 0), (1, 1), (2, 0), (2, 1)], 3), ([(1, 2), (2, 2), (1, 0), (0, 1), (0, 2)], [(0, 0), (1, 1), (2, 0), (2, 1)], 3), ([(0, 0), (2, 2), (1, 0), (0, 1), (…, …)], [(1, 2), (1, 1), (2, 0), (2, 1)], 3), ([(0, 0), (2, 2), (1, 0), (…, …), …, ……], [(1, 1), (1, 2), (2, 0), (…, …)], 3), ([(1, 1), (1, 0), (…, …), …, ……], [(2, 2), (0, 0), (…, …), …, ……], 3), ([(1, 2), (…, …), …, ……], [(2, 2), (…, …), …, ……], 3), ([(…, …), …, ……], [(…, …), …, ……], 3), ([…, ……], […, ……], 3), (…, …, …), …, ……] :: [State]

Pontozás