Logo

Főoldal

Blog

Elmélet

3D Studio

Adatvédelmi nyilatkozat

GY.I.K.

Házirend

Szerző: Marosi Csaba / marosi.csaba@3d-studio.hu

DiscordGitHubLinkedIn
Developedia
Developedia
/Tutorialok
Tutorialok
/Póker
Póker
Póker
Póker

Póker

Építsünk meg egy póker játék alapjait!

A feladat célja kifejezetten az, hogy most ne foglalkozzunk a Unity-vel kapcsolatos játékfejlesztői részletekkel. Csak a szükséges típusokat és algoritmusokat definiáljuk. Ez sem lesz feltétlenül egyszerű, de jó gyakorlat a C# ismereteik berögzítésére.

Haladjunk lépésenként, és javaslom, az én megoldásom megnézése előtt próbáld implementálni saját megoldásodat vagy legalább gondold át, hogyan állnál neki!

Lássunk hozzá!

Francia kártya

Először is definiáljunk egy típust, ami egy francia kártya lapot reprezentál! Erre mindenképp szükségünk lesz.

Hogy mi a lap értéke és színe, azt egy-egy enum változó tartalmazza. Az ehhez szükséges felsorolt típusokat is definiáljuk!

‣
Kapcsolódó anyag

Enum: a felsorolt típusEnum: a felsorolt típus

Összetett típusok és példányaikÖsszetett típusok és példányaik

‣
Megoldás
class Card
{
	public CardValue value;
	public CardColor color;
}

enum CardValue { _2, _3, _4, _5, _6, _7, _8, _9, _10 , J, Q, K, A }

enum CardColor { Spades, Hearts, Clubs, Diamonds}

52 lapos pakli

Írjunk most egy metódust, ami egy paraméterben kapott listát feltölt az franciakártya 52 lapjával!

‣
Kapcsolódó anyag

Enum: a felsorolt típusEnum: a felsorolt típus

Listák és a Foreach ciklusListák és a Foreach ciklus

‣
Megoldás

A megoldásomat egy külön statikus osztályban valósítottam meg, így a metódus is statikus. Emellett definiáltam két static readonly tömböt is, amik az összes CardValue és az összes CardColor értéket tartalmazzák:

Mi az, hogy statikus osztály és metódus? Statikus tagok és osztályokStatikus tagok és osztályok Hogyan kell egy enum típus minden elemét lekérni? Enum: a felsorolt típusEnum: a felsorolt típus Mi az, hogy readonly? Konstans és readonly Konstans és readonly

Keverés

Írjuk meg a metódust, ami megkever egy tetszőleges hosszú kártyapaklit!

‣
Kapcsolódó anyag

VéletlenekVéletlenek

‣
Megoldás
public static void ShuffleDeck(List<Card> deck)
{
		for (int i = 0; i < deck.Count; i++)
		{
				int j = Random.Range(0, deck.Count);
				Card temp = deck[i];
				deck[i] = deck[j];
				deck[j] = temp;
		}
}

Lapok rendezése

Írjuk meg a metódust, ami sorba rendez egy tetszőleges hosszú paklit

‣
Kapcsolódó anyag

Sorozatok rendezéseSorozatok rendezése

‣
Megoldás
public static void Sort(List<Card> deck)
{
		deck.Sort(CompareCards);
}

static int CompareCards(Card a, Card b)
{
		int compareByValue = a.value.CompareTo(b.value);
		if (compareByValue != 0) return compareByValue;
		return a.color.CompareTo(b.color);
}

Lapok kiíratása

Most írjunk metódust, ami egyszerű és jól olvasható formában kiír egy tetszőleges kártya listát!

‣
Kapcsolódó anyag

Összetett típusok tagmetódusaiÖsszetett típusok tagmetódusai

Listák és a Foreach ciklusListák és a Foreach ciklus

Absztrakció, Polimorfizmus (Hamarosan) - Objektum- vs. Komponens-orientált programozásAbsztrakció, Polimorfizmus (Hamarosan) - Objektum- vs. Komponens-orientált programozás

‣
Megoldás

Írjuk meg először egy kártyához az ideális kiíratást! Ehhez legcélszerűbb a Card típus ToString() metódusát felüldefiniálni.

Ezután írjuk meg a kiírató függvényt egy tetszőleges Card listára!

static void LogCards (List<Card> cards)
{
	return string.Join(", ", hand.ToString());   //   .ToString()  elhagyható
}

Póker kombinációk

A pókerben 5 lap egy “kéz”. Ezen kéznek van egy értéke, ami az alapján számolható ki, hogy milyen lapkombinációk rakhatók ki az öt lapból. Tekintsük át ezeket a póker kombinációkat és hozzunk létre egy enum típust belőlük

‣
A póker lapkombinációi érték szerint csökkenő sorrendben

Royal Flush: Öt kártya, amely ugyanolyan színű és A-tól 10-ig minden lapot tartalmaz.

Pl.: A♥ K♥ Q♥ J♥ 10♥ vagy A♣ K♣ Q♣ J♣ 10♣

Póker: Az 5 kártya tartalmaz 4 azonos értékűt.

Pl.: 7♣ 7♦ 7♥ 7♠ vagy Q♣ Q♦ Q♥ Q♠

Straight Flush: 5 lap ugyanabból a színből és az értékek sorba rendezhetők.

Pl.: 8♠ 7♠ 6♠ 5♠ 4♠ vagy 6♥ 5♥ 4♥ 3♥ 2♥

Full / Full house: Az 5 kártya tartalmaz 3 azonos értékűt és még 2 egyéb azonos értékűt.

Pl.: 10♠ 10♥ 10♦ 4♠ 4♦ vagy 9♥ 9♣ 9♠ A♥ A♣

Színsor / Flush: Öt lap ugyanabból a színből.

Pl.: A♥ Q♥ 10♥ 5♥ 3♥ vagy Q♠ 10♠ 9♠ 5♠ 2♠

Számor / Straight: Az 5 kártya értékei sorba rendezhetők.

Pl.: 8♠ 7♠ 6♥ 5♥ 4♠ vagy 6♦ 5♠ 4♦ 3♥ 2♣

Drill / Three of a kind: Az 5 kártya tartalmaz 3 azonos értékűt.

Pl.: 8♠ 8♥ 8♦ vagy 5♣ 5♥ 5♦

Két Pár / Two pair: Az 5 kártya tartalmaz 2-szer 2 azonos értékűt.

Pl.: 4♠ 4♣ 3♠ 3♥ vagy J♦ J♠ 10♠ 10♣

Egy Pár / Pair: Az 5 kártya tartalmaz 2 azonos értékűt.

Pl.: 10♥ 10♦ vagy 2♣ 2♠

Magas lap / high card: Bármi egyéb lap lapkombináció hiánya. Értéke a legmagasabb lapéval felel meg.

‣
Megoldás
public enum PokerCombination 
{ 
	RoyalFlush, Poker, StraightFlush, FullHouse, Flush,
	Straight, ThreeOfAKind, TwoPairs, Pair, HighCard 
}

Póker kombináció tesztfüggvények

Írjunk függvényt a fenti összes lapkombinációóhoz, kivéve a magas lap! Minden esetben az adott függvény öt lapról, hogy a megfelelő lapkombinációhoz tartozik-e.

Pl.: bool IsRoyalFush(List<Card> hand) { … }

Mivel egy lapot több kombináció-ra is tesztelni fogunk ezért érdemes lesz majd ezen tesztelési ciklus előtt csak egyszer rendezni, ezért:

  • A megvalósított függvények mindegyike feltételezi, hogy a bemeneti lista 5 elemű és rendezett!

Mivel ha találtunk egy magas értékű kombinációt, már nem érdekel minket az alacsonyabb, ezért:

  • A függvények mindegyike csak akkor kell, hogy helyes eredményt adjon, ha a magasabb értékű kombinációk már ki lettek zárva.
  • Pl.: Nem baj ha egy Full House-re a Drill lekérdező függvény is true-t ad vissza.

‣
Megoldás - Royal Flush, Straight Flush, Straight & Flush
‣
Megoldás - Poker, Full House, Drill, Pair, Two Pair

Először egy segédfüggvényt írok, ami megnézi sorban a lapokat és visszaadja, hogy

  • Van-e legalább 2 azonos értékű lap egymás után, (return)
  • mi a legnagyobb azonos értékű lapok száma egymás után, (out paraméter)
  • hány pár van a kézben. (out paraméter)

Felhasználom a fenti függvényt a poker, full house, drill, két pár, pár kombinációk azonosítására

Póker kezek értékei

A pókerben nem csak az számít, milyen típusú kombináció van a kézben.

Azonos típusú kombinációk esetén a kéz bizonyos értékei döntenek. Ennek részletes szabályait a legördülő menü alatt olvashatod minden egyes kombinációra külön-külön. Mindazonáltal erre most nincs szükség, mivel egyetlen összevont szabállyal is megfogalmazható, két megegyező típusú kézről, hogy melyik az erősebb. Mivel ezen univerzális szabályt rövidebben lehet szóban megfogalmazni, ezért lekódolni is ezt rövidebb, egyszerűbb.

‣
Részletes leírás (NEM javasolt megközelítés)

Royal Flush: Minden Royal Flush egyformán magas értékű

Póker: Több Póker esetén a sorban következő értékek döntenek:

  1. A 4 azonos értékű kártya értéke.
  2. Az 5. lap értéke.

Straight Flush: Több Straight Flush esetén a következő érték dönt:

  1. A sor legmagasabb értéke.

Full / Full house: Több Full house sorban esetén a következő értékek döntenek:

  1. A 3 azonos értékű kártya értéke.
  2. A 2 azonos értékű kártya értéke.

Színsor / Flush: Több Flush esetén sorban a következő értékek döntenek:

  1. A legmagasabb érték a sorban.
  2. A második legmagasabb érték a sorban.
  3. A harmadik legmagasabb érték a sorban.
  4. Mi a negyedik legmagasabb érték a sorban.
  5. Mi a ötödiklegmagasabb érték a sorban.

Számor / Straight: Több Straight esetén a következő érték dönt:

  1. A legmagasabb érték a kézben.

Drill / Three of a kind: Több drill esetén sorban a következő értékek döntenek:

  1. A 3 azonos értékű kártya értéke.
  2. A két egyéb lap közül a magasabb értékű.
  3. A két egyéb lap közül az alacsonyabb értékű.

Két Pár / Two pair: Több két pár esetén sorban a következő értékek döntenek:

  1. A 2 párból a magasabb értéke.
  2. A 2 párból az alacsonyabb értéke.
  3. Az ötödik lap értéke.

Egy Pár / Pair: Több pár esetén sorban a következő értékek döntenek:

  1. A 2 azonos értékű kártya értéke.
  2. A két egyéb lap közül a magasabb értékű.
  3. A két egyéb lap közül az alacsonyabb értékű.

Magas lap / high card: Több magas lap esetén sorban a következő értékek döntenek:

  1. A legmagasabb érték a kézben.
  2. A második legmagasabb érték a kézben.
  3. A harmadik legmagasabb érték a kézben.
  4. A negyedik legmagasabb érték a kézben.
  5. A ötödiklegmagasabb érték a kézben.
‣
Univerzális szabály (Javasolt megközelítés)

Két azonos típusú kombináció esetén rakjuk sorba az egyes kezek lapjait a következő szabály szerint:

  • Ha két értékből különböző számú lapból van a kézben, akkor azok kerülnek előre, amikből több van.
  • Ha két értékből azonos számú van, akkor a magasabb érték kerül előre.
  • Kivétel Flush (színsor) esetén: Egyszerűen érték szerint rendezzük a lapokat.
‣
Például:
  • Egy póker esetén a 4 azonos értékűlap kerül előre és utána az ötödik: 7♣ 7♦ 7♥ 7♠ Q♣
  • Egy Full House esetén a 3 azonos értékűlap kerül előre utána a 2 azonos értékű: 9♥ 9♣ 9♠ A♥ A♣
  • Egy Drill esetén a 3 azonos értékűlap kerül előre utána, utána maradékból a magasabb és utána az alacsonyabb: 3♥ 3♣ 3♠ A♥ K♣
  • Két pár esetén a magasabb pár kerül előre, utána a gyengébb és utána a maradék.
  • 7♣ 7♦ 4♦ 4♥ J♠

  • Ahol nincsenek azonos értékű lapok, vagy Flush (színsor) van a kézben ott egyszerűen sorba rendezzük a lapokat: A♦ J♣ 10♦ 4♠ 2♥ J♦ 7♦ 7♦ 4♦ 4♦

Ezután sorban haladunk elölről és összehasonlítjuk a két kéz értékeit. Ha találunk azonos indexen kettőt amelyik nem egyezik, akkor a magasabb nyert.

Most írjuk meg a függvényt, ami az “univerzális” szerint sorba rakja a kártyákat!

‣
Kapcsolódó anyag

Sorozatok rendezéseSorozatok rendezése

Egyéb adatszerkezetekEgyéb adatszerkezetek

‣
Megoldás

A PokerHand típus

Most definiáljuk a póker kéz típust.

A PokerHand típus konstruktora egy 5 elemű listáját kapja meg lapoknak. Ebből kiszámolja a konstruktor a kézben lévő póker kombinációt és az előző feladatban megírt függvénnyel sorba állított listát a lapokból.

A konstruktor kapja meg a játékos nevét is.

‣
Kapcsolódó anyag

Sorozatok rendezéseSorozatok rendezése

Konstans és readonly Konstans és readonly

‣
Megoldás

A függvény egy int-tel tér vissza a következő logika szerint:

-1 : Az első paraméterben megadott kéz erősebb 0: A két kéz egyforma erős 1: A második paraméterben megadott kéz erősebb

Póker kezek összehasonlítása

Definiáljunk egy összehasonlító függvényt, amivel majd sorba tudunk tenni kezeket érték szerint!

‣
Kapcsolódó anyag

Sorozatok rendezéseSorozatok rendezése

‣
Megoldás

Kéz kiíratása

Definiáljuk felül a Hand típus ToString() metódusát is!

‣
Kapcsolódó anyag

Összetett típusok tagmetódusaiÖsszetett típusok tagmetódusai

Listák és a Foreach ciklusListák és a Foreach ciklus

Absztrakció, Polimorfizmus (Hamarosan) - Objektum- vs. Komponens-orientált programozásAbsztrakció, Polimorfizmus (Hamarosan) - Objektum- vs. Komponens-orientált programozás

‣
Megoldás
public override string ToString() =>
		playerName + " : " +
		string.Join(", ", cardsSortedToCompare) + "  -  " +
		pokerCombination;

Póker játszma

Most végre játsszuk le a játszmát!

Ehhez írjunk függvényt, ami tetszőleges számú játékosra kioszt lapokat (ötöt fejenként) és sorba teszi a játékosokat a kezük erőssége szempontjából, majd kiírja a kezeket és a játékosok sorrendjét!

‣
Megoldás

És most teszteljük le!

PlayPokerMatch("Homer", "Moe", "Barney", "Carl", "Lenny");
image
image
‣

Letölthető forráskód itt

Card.cs1.0KB
CardUtility.cs1.0KB
PlayPoker.cs1.1KB
PokerHand.cs2.6KB
PokerTester.cs1.0KB
PokerUtility.cs5.8KB
public class CardUtility // Kártya osztályhoz tartozó segédmetódusok:
{
		static readonly CardValue[] allValues =
				(CardValue[])System.Enum.GetValues(typeof(CardValue));
		
		static readonly CardColor[] allColors =
				(CardColor[])System.Enum.GetValues(typeof(CardValue));
		
		// A feladatban leírt függvény:
		public static void FillDeck(List<Card> deck)
		{
				foreach (CardColor c in allColors)
					{			
							foreach (CardValue v in allValues)
							{
									deck.Add(new Card() { value = v, color = c });
							}
					}
		}
}
// Card típuson belül:
public override string ToString()
	{
		char colorChar = color switch
		{
			CardColor.Spades => '♠',
			CardColor.Hearts => '♥',
			CardColor.Clubs => '♣',
			CardColor.Diamonds => '♦',
			_ => '?'
		};

		string valueString = value switch
		{
			CardValue._2 => "2",
			CardValue._3 => "3",
			CardValue._4 => "4",
			CardValue._5 => "5",
			CardValue._6 => "6",
			CardValue._7 => "7",
			CardValue._8 => "8",
			CardValue._9 => "9",
			CardValue._10 => "10",
			CardValue.J => "J",
			CardValue.Q => "Q",
			CardValue.K => "K",
			CardValue.A => "A",
			_ => "?"
		};

		return valueString + colorChar;
	}
public static bool IsRoyalFlush(List<Card> hand)
{
	return IsStraightFlush(hand) && hand[0].value == CardValue._10;
}

public static bool IsStraightFlush(List<Card> hand)
{
	return IsStraight(hand) && IsFlush(hand);  // Lapok egyszínűek-e?
}

public static bool IsStraight(List<Card> hand)
{
	int lastValue = (int)hand[0].value;
	for (int i = 1; i < hand.Count; i++)
	{
		int currentValue = (int)hand[i].value;
		if (currentValue != lastValue + 1) return false;
		lastValue = currentValue;
	}
	return true;
}

public static bool IsFlush(List<Card> hand)
{
	CardColor firstColor = hand[0].color;
	for (int i = 1; i < hand.Count; i++)
	{
		if (hand[i].color != firstColor)
			return false;
	}
	return true;
}
// Ez a függvény megnézi sorban a lapokat és visszaadja, hogy
//   - Van-e legalább 2 azonos értékű lap egymás után,          (return)
//   - mi a legnagyobb azonos értékű lapok száma egymás után,	(out)
//   - hány pár van a kézben.									(out)

static bool CardsInRowTest(List<Card> hand,
	out int longestInRow, out int numberOfPairs)
{
	// Felételezem, hogy a bemenet egy sorba rendezett lista,
	// így elölről haladok végig.

	longestInRow = 0; // Sorban a legtöbb azonos értékű lap
	numberOfPairs = 0;  // Párok száma.

	// Végig lépegetek az összes lapon:
	for (int i = 0; i < hand.Count;)  // (Ciklusváltozót most nem itt növelem!)
	{
		int sameValueCount = 1; // Zsinórban azonos értékű lapok száma.

		// Végig nézem a következő lapokat.
		for (int j = i + 1; j < hand.Count; j++)
		{
			if (hand[i].value == hand[j].value) // Ha azonos értékűek,
				sameValueCount++;                 // akkor növelem a számlálót.
			else                                // Egyébként
				break;                            // kilépek a ciklusból.
		}

		// Ha ez eddig a a leghosszabb azonos értékű sorozat, ...
		if (sameValueCount > longestInRow)
			longestInRow = sameValueCount;   // akkor elteszem ezt a maximumnak

	  // Ha a zsinórban azonos értékű lapok száma 2, ....
		if (sameValueCount == 2)
			numberOfPairs++;        // akkor növelem a párok számát.

		i += sameValueCount;  // Növelem a ciklusváltozót annyival,
                          // amennyi azonos értékű lap volt sorban.
	}

	// Visszatérek egy bool értékkel:
	return longestInRow > 1;	// A leghosszabb sorozat legalább 2 lapból áll.
}
// Ha legnagyobb azonos értékű lapok száma 4, akkor póker.
public static bool IsPoker(List<Card> hand) =>
	CardsInRowTest(hand, out int longestInRow, out int _) &&
	longestInRow == 4;

// Ha a legnagyobb azonos értékű lapok száma 3,
// és van egy párom is, akkor full house.
public static bool IsFullHouse(List<Card> hand) =>
	CardsInRowTest(hand, out int longestInRow, out int numberOfPairs) &&
	longestInRow == 3 &&
	numberOfPairs == 1;

// Ha a legnagyobb azonos értékű lapok száma 3,
// és nincs párom, akkor drill.
public static bool IsDrill(List<Card> hand) =>
	CardsInRowTest(hand, out int longestInRow, out int numberOfPairs) &&
	longestInRow == 3 &&
	numberOfPairs == 0;

// Ha a legnagyobb azonos értékű lapok száma 2
// és csak egy párom, akkor pár.
public static bool IsPair(List<Card> hand) =>
	CardsInRowTest(hand, out int longestInRow, out int numberOfPairs) &&
	longestInRow == 2 &&
	numberOfPairs == 1;

// Ha a legnagyobb azonos értékű lapok száma 2,
// és két párom van, akkor két pár.
public static bool IsTwoPairs(List<Card> hand) =>
	CardsInRowTest(hand, out int longestInRow, out int numberOfPairs) &&
	longestInRow == 2 &&
	numberOfPairs == 2;
void SortToCompare(List<Card> cardsSorted)
{
		if (PokerUtility.IsFlush(cardsSorted)) // Speciális eset:  Flush (Színsor)
		{
			cardsSorted.Sort((x, y) => y.value.CompareTo(x.value));
			return;
		}

		Dictionary<CardValue, List<Card>> cardsOfTheSameValue = new();

		foreach (Card card in cardsSorted)
		{
			CardValue value = card.value;
			if (!cardsOfTheSameValue.ContainsKey(value))
				cardsOfTheSameValue.Add(value, new List<Card>());

			cardsOfTheSameValue[value].Add(card);
		}

		List<List<Card>> cardsOfTheSameValueSorted = new();

		foreach (KeyValuePair<CardValue, List<Card>> pair in cardsOfTheSameValue)
		{
			List<Card> cards = pair.Value;
			cardsOfTheSameValueSorted.Add(cards);
		}

		cardsOfTheSameValueSorted.Sort(SortByLengthOfTheList);

		cardsSorted.Clear();
		foreach (List<Card> cards in cardsOfTheSameValueSorted)
			cardsSorted.AddRange(cards);
}

// -1 :      Az első paraméterben megadott lista kerül előrébb
// 0:        A két kéz egyforma erős
// 1:        A második paraméterben megadott lista kerül előrébb
int SortByLengthOfTheList(List<Card> x, List<Card> y)
{
		int byLength = y.Count.CompareTo(x.Count);
		if (byLength != 0)
			return byLength;

		return y[0].value.CompareTo(x[0].value);
}
public class PokerHand
{
	public readonly string playerName;
	public readonly PokerCombination pokerCombination;
	public readonly List<Card> cardsSortedToCompare;

public PokerHand(string playerName, List<Card> hand)
{
		this.playerName = playerName;

		if(hand.Count != 5)
			Debug.LogError("This constructor works only with 5 card!");

		List<Card> sorted = new();
		sorted.AddRange(hand);

		CardUtility.SortByValue(sorted);
		pokerCombination = PokerUtility.GetPokerCombination(sorted);

		SortToCompare(sorted);
		cardsSortedToCompare = sorted;
	}
}
// -1 :      Az első paraméterben megadott kéz erősebb
// 0:        A két kéz egyforma erős
// 1:        A második paraméterben megadott kéz erősebb

public static int CompareHands(PokerHand a, PokerHand b)
{

		// Ha a két kéz különböző kombinációval rendelkezik, akkor a magasabb kombináció nyer.
		int compareByCombination = b.pokerCombination.CompareTo(a.pokerCombination);
		if (compareByCombination != 0)
			return compareByCombination;

		// Ha a két kéz ugyanazzal a kombinációval rendelkezik, akkor a kártyák értékei döntenek.
		for (int i = 0; i < a.cardsSortedToCompare.Count; i++)
		{
			int compareByValue = a.cardsSortedToCompare[i].value.CompareTo(b.cardsSortedToCompare[i].value);
			if (compareByValue != 0)
				return compareByValue;
		}

		// Ha a két kéz ugyanazzal a kombinációval és ugyanazokkal az értékekkel rendelkezik, akkor döntetlen.
		return 0;
}
void PlayPokerMatch(List<string>playerNames)
{
		List<Card> deck = new();        // Kártyapakli létrehozása, 
		CardUtility.FillDeck(deck);     // feltöltése
		CardUtility.ShuffleDeck(deck);  // és megkeverése

		List<PokerHand> players = new();  // Játékosok listája.

		for (int i = 0; i < playerCount; i++)     // Minden játékos...
		{
			List<Card> cards = new();
			for (int j = 0; j < 5; j++) // kap öt lapot...
			{
				if (deck.Count == 0)
					return;

				cards.Add(deck[0]);       // a pakli tetejéről.
				deck.RemoveAt(0);         // (Ne felejtsük el kivenni a lapokat a pakliból)
			}

			PokerHand hand = new(cards);           // Létrehozzuk a kezet.
			players.Add(hand); 
		}
	
		// Sorbarendezzüka játékosokat a kezeik erőssége szerint
		players.Sort(PokerHand.CompareHands);

		// Majd ebben a sorrenben kiíratjuk őket (Legerősebbel kezdünk) 
		for (int i = players.Count - 1; i >= 0; i--)
					Debug.Log(players[i]);
}