Kő-Papír-Olló 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

Ebben a feladatban a Kő-Papír-Olló kétszemélyes játékot fogjuk modellezni, különböző stratégiákat adunk meg a játék játszásához, majd összehasonlítjuk ezeket.

A játék menete:

A jelek ütési sorrendje:

Az egyszerűség kedvéért a jeleket (Sign) egész értékeknek feleltetjük meg, azaz a kő, papír és olló jeleket rendre a 0, 1 és 2 értékek fogják jelenteni.

type Sign = Int

Jel vizsgálata (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 jelekre, ha mind a kettő megfelel a jelekre vonatkozó megszorításunknak! Ellenkező esetben használjuk az error függvényt (a példákban látható módon)!

validSigns :: (Sign -> Sign -> a) -> Sign -> Sign -> a

Test>
1 :: Sign
Test>
2 :: Sign
Test>
⊥₁ :: (Sign, Sign)
⊥₁: validSigns: invalid values: (10,0)
CallStack (from HasCallStack):
  error, called at ./RPSGame.lhs:80:27 in main:RPSGame
Test>
⊥₁ :: (Sign, Sign)
⊥₁: validSigns: invalid values: (2,-1)
CallStack (from HasCallStack):
  error, called at ./RPSGame.lhs:80:27 in main:RPSGame

Ütés vizsgálata (1 pont)

Adjuk meg azt a függvényt, amely eldönti, hogy a bevezetőben ismertetett sorrend szerint az első argumentumban szereplő jel üti-e a második argumentumban szereplő jelet!

Ügyeljünk arra, hogy csak a feladatnak megfelelő értékeket fogadjuk el!

beats :: Sign -> Sign -> Bool

Test>
True :: Bool
Test>
[(0, 0, False), (0, 1, False), (0, 2, True), (1, 0, True), (1, 1, False), (1, 2, False), (2, 0, False), (2, 1, True), (2, 2, False)] :: [(Int, Int, Bool)]
Test>
⊥₁ :: Bool
⊥₁: validSigns: invalid values: (4,0)
CallStack (from HasCallStack):
  error, called at ./RPSGame.lhs:80:27 in main:RPSGame
Test>
⊥₁ :: Bool
⊥₁: validSigns: invalid values: (1,3)
CallStack (from HasCallStack):
  error, called at ./RPSGame.lhs:80:27 in main:RPSGame

Döntetlen vizsgálata (1 pont)

Adjuk meg azt a függvényt, amely eldönti, hogy az első és a második argumentumként kapott jel döntetlen játékot eredményez-e!

Ügyeljünk arra, hogy csak a feladatnak megfelelő értékeket fogadjuk el!

isDraw :: Sign -> Sign -> Bool

Test>
True :: Bool
Test>
[(0, 0, True), (0, 1, False), (0, 2, False), (1, 0, False), (1, 1, True), (1, 2, False), (2, 0, False), (2, 1, False), (2, 2, True)] :: [(Int, Int, Bool)]
Test>
⊥₁ :: Bool
⊥₁: validSigns: invalid values: (4,5)
CallStack (from HasCallStack):
  error, called at ./RPSGame.lhs:80:27 in main:RPSGame

Egy kör eredménye (1 pont)

Definiáljuk az egy kört kiértékelő függvényt! A függvény 1, 0 és -1 értéket adjon vissza annak megfelelően, hogy az első játékos nyert, döntetlent ért el, vagy veszített.

result :: Sign -> Sign -> Int

Test>
-1 :: Int
Test>
1 :: Int
Test>
[0, -1, 1, 1, 0, -1, -1, 1, 0] :: [Int]

Egy játszma kiértékelése (1 pont)

Minden játszma több körből áll. A körökben használt jeleket minden játékosról számon tartjuk, hogy később az eredmény kihirdetése során fel tudjuk ezt használni. A jelek sorrendje fontos, így ezt a sorozatot listával reprezentáljuk:

type Play = [Sign]

A lista elején található a legutóbbi körben felmutatott jel, az azt követő az eggyel korábbi körből való és így tovább.

Egy játszmát és annak aktuális állását az eddigi megadott jelek sorozatainak rendezett párjaként adjuk meg:

type Rounds = (Play, Play)

A rendezett pár első eleme az első játékos, a második eleme pedig a második játékos eddig felmutatott jeleinek sorozata (Play). A Rounds típusú értékeknél mindig feltehetjük, hogy a benne foglalt két lista azonos hosszúságú.

Adjuk meg azt a függvényt, amely összegez egy játszmát és megadja annak eredményét! A függvény eredménye:

tournament :: Rounds -> Int

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

Játszma szétosztása (2 pont)

Adjuk meg azt a függvényt, amely egy játszma eddigi eredményeit az első játékos szempontjából három részre osztja: nyerő, döntetlen és vesztes lépések! A függvény egy rendezett hármasként adja meg az eredményt, ahol a rendezett hármas a következő elemekből épül fel:

partitionRounds :: Rounds -> ([Sign], [Sign], [Sign])

Test>
([], [], []) :: ([Sign], [Sign], [Sign])
Test>
([1, 1], [2, 1], [0, 2, 0, 1, 0, 1]) :: ([Sign], [Sign], [Sign])
Test>
([1, 0, 1, 2, 1, 2], [2, 1], [0, 0]) :: ([Sign], [Sign], [Sign])

Rákövetkező előállítása (1 pont)

Adjuk meg azt a függvényt, amely ütés szempontjából megadja a megadott jel rákövetkezőjét ( papír, papír olló, olló )!

next :: Sign -> Sign

Test>
[1, 2, 0] :: [Sign]

Elem előfordulási gyakorisága (1 pont)

Adjuk meg azt a függvényt, amely meghatározza egy elem előfordulási gyakoriságát egy adott listán belül!

frequency :: Eq a => a -> [a] -> Int

Test>
0 :: Int
Test>
6 :: Int
Test>
4 :: Int

Leggyakoribb elem (2 pont)

Adjuk meg azt a függvényt, amely megadja, hogy melyik a leggyakrabban előforduló elem egy listában! Amennyiben több elem is azonos gyakorisággal fordul elő, az érték szerinti legnagyobbat adjuk meg!

mostFrequent :: Ord a => [a] {-nem üres-} -> a

Test>
0 :: Integer
Test>
2 :: Integer
Test>
2 :: Integer

Stratégiák

Bevezetjük a Strategy szinonimát, amely egy függvénytípust ír le. A Strategy típusú műveletek argumentumként megkapják az első és a játékostársunk eddigi lépéseinek sorozatait (Rounds) és megpróbálják az első játékos szemszögéből meghatározni a következő lépést arra törekedve, hogy ez nyerő (vagy legalább döntetlen) legyen.

type Strategy = Rounds -> Sign -- (Play, Play) -> Sign

A következő függvények segítségével három különböző stratégiát fogalmazunk meg, amelyek a korábbi lépések felhasználásával próbálnak meg javaslatot adni a következő lépésre (az első játékos szemszögéből).

Megjegyzés: Ne feledjük, a jelek olyan sorrendben vannak, hogy a rákövetkezés egyben mindig definiálja az erősebbet. Vagyis a-t mindig ütni fogja a rákövetkezője.

Első stratégia (1 pont)

Az első stratégia szerint megnézzük, hogy a legutóbbi lépésben választott a jellel nyerünk, vagy veszítünk az ellenfél b jelével szemben:


Test>
0 :: Sign
Test>
0 :: Sign
Test>
1 :: Sign
Test>
0 :: Sign
Test>
1 :: Sign
Test>
2 :: Sign

Második stratégia (1 pont)

A második stratégiát az ellenfél lépéseire alapozzuk. Megnézzük, hogy az ellenfél eddigi lépései során melyik jelet használta leggyakrabban (legyen ez b) és ennek megfelelően választjuk meg a következő lépésünket, azaz a b-t ütő jelet adjuk vissza. Amennyiben ez az első kör, az ollónak megfelelő értéket (2) adjuk vissza!


Test>
2 :: Sign
Test>
0 :: Sign
Test>
0 :: Sign
Test>
0 :: Sign

Harmadik stratégia (3 pont)

A harmadik stratégia kicsit összetettebb, mely a saját lépéseink elemzését használja fel a következő lépés meghatározásához.

Amennyiben ez az első kör, az papírnak megfelelő értéket (1) adjuk vissza! Ellenkező esetben első lépésként felosztjuk a saját lépéseinket három részre annak megfelelően, hogy nyertünk, döntetlent játszottunk, vagy vesztettünk az aktuális lépésekkel (ne feledjük, volt ilyen műveletünk korábban). Ezek után a következő eseteket különböztetjük meg:


Test>
1 :: Sign
Test>
0 :: Sign
Test>
0 :: Sign
Test>
0 :: Sign
Test>
2 :: Sign

Következő lépés előállítása (1 pont)

Adjuk meg azt a függvényt, amely két stratégia megadásának felhasználásával megadja, hogy mit fog lépni a két játékos! Az első stratégia az első játékosra vonatkozik a második pedig a második játékosra!

A második játékos lépésének meghatározásánál ne feledkezzünk meg arról, hogy a megadott stratégia mindig a rendezett pár első eleme szerint határozza meg az optimális lépést!

applyStrategies :: Strategy -> Strategy -> Rounds -> Rounds

Test>
([0], [2]) :: Rounds
Test>
([2, 0], [1, 1]) :: Rounds
Test>
([1, 2, 0], [0, 0, 1]) :: Rounds
Test>
([1, 2, 0, 1, 0], [1, 0, 1, 1, 0]) :: Rounds

N-lépéses játék (1 pont)

Adjuk meg azt a függvényt, amely a megadott lépésszámú játékot lejátssza a megadott stratégiák felhasználásával!

play :: Int -> Strategy -> Strategy -> Rounds

Test>
([], []) :: Rounds
Test>
([], []) :: Rounds
Test>
([0, 2, 1, 0, 2, 1, 0, 1, 2, 0], [0, 2, 1, 0, 2, 1, 0, 0, 1, 2]) :: Rounds
Test>
2 :: Int
Test>
[0, 1, 2, 2, 0, 1, 1, 2, 0] :: [Int]

Nyerő stratégia kiválasztása (1 pont)

Tegyük fel, hogy a soron következő ellenfelünk korábbi játszmái alapján sikerült meghatároznunk annak stratégiáját. Szerencsénkre ez az általunk korábban kidolgozott stratégia egyike, így azt könnyedén össze tudjuk vetni a többi stratégiánkkal és kiválaszthatjuk az ezzel szembeni nyerőt.

Adjuk meg azt a függvényt, amely egy ismert stratégiához megadja a vele szembeni nyerő stratégiát 10-menetes játszmák esetén! A stratégiákat egytől számozzuk. Az eredményt egy listában adjuk vissza, ha tehát nincs ilyen stratégia, akkor az eredmény üres lista lesz.

winningStrategy :: Strategy -> [Strategy] -> [Int]

Test>
[3] :: [Int]
Test>
[3] :: [Int]
Test>
[1] :: [Int]
Test>
[2] :: [Int]

Pontozás