PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Rozdanie kart do gry w brydża Formularz aplikacji i implementacja talii kart Tasowanie i rozdanie kart czterem graczom Prezentacja rozkładu Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Rozdanie kart do gry w brydża

Brydż jest logiczną grą karcianą, w której uczestniczy czterech graczy siedzą­cych naprze­ciwko siebie i tworzą­cych dwie rywali­zujące pary N-S (ang. north-south, północ-południe) i E-W (ang. east-west, wschód-zachód):

     N
  W     E
     S

Do gry używana jest standardowa talia 52 kart obejmująca cztery kolory (trefl – ♣, karo – , kier – i pik – ♠), po 13 kart w każdym kolorze (od dwójki do asa). Potasowane i przeło­żone karty gracz rozda­jący rozdaje po jednej zgodnie z ruchem wskazówek zegara, poczy­nając od gracza po lewej stronie. Każdy gracz otrzymuje więc po 13 kart. Przyjęło się układać karty kolorami i porząd­kować w ich obrębie według starszeń­stwa.

W aplikacji okienkowej o nazwie Brydz symulu­jącej rozdanie kart do gry w brydża potra­ktujemy talię 52 kart jako listę klasy genery­cznej List złożonej z obiektów klasy Karta zawiera­jącej trzy skła­dniki – dwa pola określa­jące kolor karty i jej wysokość oraz inicjali­zujący je konstru­ktor. Program będzie bardziej czytelny, gdy w oddzielnym pliku oprócz klasy Karta reprezen­tującej elementy listy zdefi­niujemy trzy typy wylicze­niowe symboli­zujące kolory kart, ich wyso­kości i czterech graczy. Zatem do projektu apli­kacji dodajemy nowy składnik – plik Karty.cs postaci:

namespace Brydz
{
    enum Kolor { Trefl, Karo, Kier, Pik }

    enum Wysok
    {
        Dwójka, Trójka, Czwórka, Piątka, Szóstka, Siódemka, Ósemka, Dziewiątka,
        Dziesiątka, Walet, Dama, Król, As
    }

    enum Gracz { West, North, East, South }

    class Karta
    {
        public Kolor kol;
        public Wysok wys;
        public Karta(Kolor k, Wysok w)
        {
            kol = k;
            wys = w;
        }
    }
}

Elementami klas wyliczeniowych enum są nazwane stałe o warto­ściach całko­witych. Jeśli nie są jawnie określone jak w rozpatry­wanym przypadku, są nimi kolejne liczby od zera wzwyż. W prezento­wanych poniżej programie używana jest tylko pierwsza i ostatnia ze stałych typu Wysok, dlatego jego defi­nicja mogłaby zostać skrócona do postaci:

enum Wysok { Dwójka, As = 12 }

Formularz aplikacji i implementacja talii kart

Rozbudowę utworzonego przez Visual Studio formularza aplikacji rozpoczy­namy od usta­wienia szeregu jego właści­wości: tytułu Text na Rozdanie kart do gry w brydża, koloru tła BackColor na Info (jasny, żółto-kremowy), stylu okna FormBorder­Style na Fixed­Single (poje­dyncza stała ramka) i jego maksyma­lizacji Maxi­mizeBox na False (blokada). Następnie wsta­wiamy kompo­nent MenuStrip i tworzymy proste menu z dwoma polece­niami RozdajZakończ (rys.). Pierwsze ma służyć do wygene­rowania kolejnego rozdania kart, drugie do zakoń­czenia apli­kacji.

Nieco żmudnym etapem projektowania formularza jest umieszczenie w jego obszarze robo­czym 36 etykiet (kompo­nentów Label) i sprecyzo­wanie ich wyglądu. Znaki kolorów kart (pik, trefl, kier i karo) wybieramy w tablicy znaków Windows (Unicode U+2660, U+2663, U+2665, U+2666) i kopiujemy do właści­wości Text stosownych etykiet. Rozmiar czcionki tekstu opisu­jącego etykiety określamy, rozwi­jając właści­wość zagnie­żdżoną Font ozna­czoną krzyżykiem (+) i nadając jej podwłaści­wości Size wartość 16 (jednoli­terowe nazwy graczy), 12 (znaki kolorów kart) i 10 (nazwy list kart graczy), zaś kolor tekstu części etykiet, usta­wiając właści­wość ForeColor na Olive (oliwkowy, nazwy graczy) i Red (czerwony, znaki kier i karo). Ponadto w celu ułatwienia dostępu do szesnastu etykiet o opisach piki_w, kiery_w itp. zmieniamy ich nazwy domyślne postaci labelxx (xx – cyfry) na nazwy bardziej sygestywne, nadając właści­wości Name tych etykiet takie same wartości jakie ma ich właści­wość Text. W gruncie rzeczy wartości właści­wości Text tych etykiet mają w fazie projekto­wania formu­larza jedynie znaczenie dokumen­tacyjne. W trakcie wykonania programu właści­wości te są wykorzy­stywane do prezen­tacji wyloso­wanego rozkładu rozdania kart.

W programie będzie używana lista generyczna obiektów klasy Karta reprezen­tująca talię kart, tablica czterech list genery­cznych uosabia­jąca układy kart na ręku każdego z graczy i gene­rator liczb losowych. Referencje do tych obiektów będą przecho­wywane w trzech polach klasy Form1:

List<Karta> talia = null;
List<Karta>[] gracz = { null, null, null, null };
Random gen;

Wszystkie obiekty najwygodniej jest utworzyć w metodzie obsługi zdarzenia Load formu­larza, które jest wyzwalane, gdy okno zostało załado­wane do pamięci i ma być po raz pierwszy wyświe­tlone. Na początku genero­wana jest pełna lista kart uporządko­wana malejąco według koloru od pika do trefla, a w ramach kolorów od asa do dwójki. Listy kart graczy są wstępnie puste. Nie precy­zując na razie operacji tasowania kart, rozda­wania ich pomiędzy czterech graczy i prezen­tacji uzyska­nego rozkładu, metodę Load możemy sformu­łować następu­jąco:

private void Form1_Load(object sender, EventArgs e)
{
    talia = new List<Karta>(52);
    for (Kolor k = Kolor.Pik; k >= Kolor.Trefl; k--)
        for (Wysok w = Wysok.As; w >= Wysok.Dwójka; w--)
            talia.Add(new Karta(k, w));
    for (Gracz g = Gracz.West; g <= Gracz.South; g++)
        gracz[(int)g] = new List<Karta>(13);
    gen = new Random();
    ...                     // Tasuj karty
    ...                     // Rozdaj karty
    ...                     // Pokaż rozkład
}

Tasowanie i rozdanie kart czterem graczom

W celu potasowania talii kart posłużymy się prostą metodą polega­jącą na zamianie miejscami każdego elementu listy talia, od pierwszego do ostatniego, z innym elementem tej listy wybie­ranym losowo. Do przesta­wianych elementów wygodnie jest odwoływać się za pomocą indeksów:

private void Tasuj_karty()
{
    for (int i = 0; i < talia.Count; i++)
    {
        int j = gen.Next(talia.Count);
        if (i != j)
        {
            Karta karta = talia[i];
            talia[i] = talia[j];
            talia[j] = karta;
        }
    }
}

Precyzując metodę rozdawania kart potaso­wanej talii czterem graczom przyjmiemy dla ustalenia uwagi, że rozda­jącym jest gracz N, który rozdaje je po jednej zgodnie z ruchem wskazówek zegara, zaczy­nając od gracza E. Operację przeno­szenia pierwszego elementu listy reprezen­tującej talię kart na koniec listy kolejnego gracza możemy zreali­zować za pomocą metody First udostępnia­jącej początkowy element listy, metody Add dodającej element na końcu listy i metody RemoveAt usuwającej z listy element o określonym indeksie. Postępo­wanie to kontynu­ujemy aż do wyczer­pania talii, zmieniając cykli­cznie graczy. Pomijając na razie kwestię porządko­wania listy kart na ręku każdego gracza, metodę rozdawania kart możemy sformu­łować następu­jąco:

private void Rozdaj_karty()
{
    for (Gracz g = Gracz.North; talia.Count > 0; )
    {
        gracz[(int)g].Add(talia.First());
        talia.RemoveAt(0);
        if (g < Gracz.South)
            g++;
        else
            g = Gracz.West;
    }
    ...                     // Uporządkuj karty na ręku każdego gracza
}

Nieco inny sposób rozdawania kart polegający na przeno­szeniu za każdym razem ostatniego elementu listy głównej daje niewielkie korzyści czasowe, gdyż nie wymaga przesu­wania jej pozosta­łych elementów. Należy wówczas zamiast metody First skorzystać z metody Last, która udostępnia ostatni element listy, a w metodzie RemoveAt zamiast indeksu 0 użyć indeksu Count–1.

Uporządkowanie listy kart każdego gracza można by osiągnąć w trakcie rozda­wania kart poprzez wstawianie nowej karty nie na końcu listy, lecz za każdym razem w odpo­wiednie jej miejsce. Wygo­dniejszym sposobem jest posorto­wanie listy za pomocą wydajnej metody Sort opartej na algo­rytmie sortowania szybkiego (ang. quick sort). Jej argu­mentem jest metoda, która porównuje dwa elementy listy i zwraca liczbę całkowitą ujemną, gdy pierwszy z tych obiektów jest mniejszy od drugiego, zero, gdy są równe, lub dodatnią, gdy pierwszy jest większy od drugiego. Zazwyczaj kod metody porównu­jącej jest krótki, co pozwala na zdefinio­wanie jej bezpo­średnio w wywo­łaniu metody sortującej jako metodę anonimową. W rozpatry­wanym przypadku karty się nie powtarzają, dlatego metoda anonimowa porównu­jąca dwa obiekty klasy Karta może zwrócić tylko wartość ujemną lub dodatnią. Listy mają być porządko­wane malejąco według klucza kolor+wysokość, toteż operację sorto­wania ich możemy zaprogra­mować następu­jąco:

foreach (List<Karta> ręka in gracz)
    ręka.Sort(delegate(Karta x, Karta y)
    {
        return (x.kol > y.kol || (x.kol == y.kol && x.wys > y.wys)) ? -1 : 1;
    });

Prezentacja rozkładu

W kolejnym kroku precyzowania programu należy skoncen­trować się na prezen­tacji wygenero­wanego losowo rozkładu kart. Listę 13 kart, jaką otrzymał gracz, przedsta­wimy w postaci układu napisów na czterech etykietach przezna­czonych w proje­kcie formu­larza dla pików, kierów, kar i trefli. Cztero­krotne wywołanie tej jeszcze niesformu­łowanej metody z odpowie­dnimi argumen­tami reprezen­tującymi listę kart gracza i tablicę czterech etykiet stanowi treść metody pokazu­jącej rozkład rozdania w oknie aplikacji:

private void Pokaż_rozkład()
{
    Układ(gracz[0], new Label[] { piki_w, kiery_w, kara_w, trefle_w });
    Układ(gracz[1], new Label[] { piki_n, kiery_n, kara_n, trefle_n });
    Układ(gracz[2], new Label[] { piki_e, kiery_e, kara_e, trefle_e });
    Układ(gracz[3], new Label[] { piki_s, kiery_s, kara_s, trefle_s });
}

Jak widać, pierwszym argumentem wywołania metody Układ jest lista kart na ręku gracza, a drugim tworzona "w locie" (bezpo­średnio w argu­mencie wywołania) tablica czterech etykiet (czterech refe­rencji do kompo­nentów typu Label). Zadaniem metody jest umieszczenie na etykietach napisów określa­jących wyso­kości kart listy z uwzglę­dnieniem podziału na piki, kiery, kara i trefle. Na wstępie metoda przypisuje właści­wości Text wszystkich etykiet łańcuch pusty, a potem przebiega po wszystkich kartach ręki gracza i dodaje do łańcucha reprezentu­jącego właści­wość Text stosownej etykiety wysokość karty i spację:

private void Układ(List<Karta> ręka, Label[] etykiety)
{
    string[] SW = { "2", "3", "4", "5", "6", "7", "8", "9", "10", "W", "D", "K", "A" };
    foreach (Label e in etykiety)
        e.Text = "";
    foreach (Karta karta in ręka)
        etykiety[(int)karta.kol].Text += SW[(int)karta.wys] + " ";
}

Stosowanie łańcuchów w sytuacjach, w których trzeba je często modyfi­kować, nie jest ze względów wydajno­ściowych zalecane, ponieważ każda taka operacja wymaga tworzenia nowego obiektu i ew. niszczenia obiektu poprze­dniego. Na przykład druga instru­kcja kodu:

string s = "Ala i As";
s += " idą do lasu";

powoduje utworzenie łańcucha wynikowego – nowego obiektu typu string, którego refe­rencja zostanie zapamię­tana w zmiennej s. Skutkiem tego refe­rencja do łańcucha pierwo­tnego zostanie utracona, co staje się sygnałem dla mecha­nizmu odśmie­cania pamięci platformy .NET, że obiekt ten ma być zniszczony. W celu przyśpie­szenia działania programu warto skorzystać z klasy String­Builder, która przecho­wuje łańcuch w buforze pamięci, zwiększając w razie potrzeby jego rozmiar. Przebu­dowana metoda Układ ma postać:

private void Układ(List<Karta> ręka, Label[] etykiety)
{
    string[] SW = { "2 ", "3 ", "4 ", "5 ", "6 ","7 ", "8 ", "9 ", "10 ",
                    "W ", "D ", "K ", "A " };
    StringBuilder[] s = { new StringBuilder(), new StringBuilder(),
                          new StringBuilder(), new StringBuilder() };
    foreach (Karta karta in ręka)
        s[(int)karta.kol].Append(SW[(int)karta.wys]);
    for (int k = 0; k < 4; k++)
        etykiety[k].Text = s[k].ToString();
}

Metoda tworzy tablicę czterech obiektów typu String­Builder reprezen­tujących łańcuchy puste, do których dodaje wysokości kart rozsze­rzone o spację (metoda Append), a na koniec zamienia je wszystkie na łańcuchy typu string (metoda ToString) i przypisuje właści­wości Text etykiet. Różnicę wydajności obu wersji metody Układ widać wyraźnie, gdy wymusi się w programie końcowym genero­wanie ciągu rozdań kart poprzez jedno­czesne naciśnięcie i przytrzy­manie klawiszy Alt+R. W przy­padku pierwszej wersji program zajęty obsługą łańcuchów nie nadąża z pokazy­waniem rozkładów kart na ekranie, zaś w drugim nie ma z tym żadnego problemu.

Program w C#

Ostatnim krokiem precyzowania programu jest sformuło­wanie metod obsługi dwóch poleceń menu – RozdajZakończ. Druga sprowadza się do wywołania metody Close formu­larza, zajmijmy się więc pierwszą. Przypo­mnijmy, że program rozpo­czyna działanie od obsługi zdarzenia Load formu­larza, która kończy się wygenero­waniem pierwszego rozdania kart. Zmienna talia zawiera wówczas refe­rencję do pustej talii kart, a tablica gracz cztery refe­rencje do list kart graczy. Zatem przed wykona­niem kolejnego rozdania należy wszystkie elementy list graczy zgromadzić w liście reprezen­tującej całą talię. Operację tę można zreali­zować, korzystając z metody AddRange dodającej elementy kolekcji na końcu listy i metody Clear usuwającej wszystkie elementy listy. Metoda obsługi zdarzenia Click pole­cenia Rozdaj może więc mieć następu­jącą postać:

private void rozdajToolStripMenuItem_Click(object sender, EventArgs e)
{
    foreach (List<Karta> ręka in gracz)
    {
        talia.AddRange(ręka);
        ręka.Clear();
    }
    Tasuj_karty();
    Rozdaj_karty();
    Pokaż_rozkład();
}

Poniżej przedstawiony jest kod źródłowy programu okienko­wego symulu­jącego rozdanie kart do gry w brydża (listing obejmuje tylko plik Form1.cs). W pro­gramie wykorzy­stano drugą, bardziej wydajną wersję metody Układ prezentu­jącej układ kart rozdania na ręku gracza.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Brydz
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            talia = new List<Karta>(52);
            for (Kolor k = Kolor.Pik; k >= Kolor.Trefl; k--)
                for (Wysok w = Wysok.As; w >= Wysok.Dwójka; w--)
                    talia.Add(new Karta(k, w));
            for (Gracz g = Gracz.West; g <= Gracz.South; g++)
                gracz[(int)g] = new List<Karta>(13);
            gen = new Random();
            Tasuj_karty();
            Rozdaj_karty();
            Pokaż_rozkład();
        }

        private void Tasuj_karty()
        {
            for (int i = 0; i < talia.Count; i++)
            {
                int j = gen.Next(talia.Count);
                if (i != j)
                {
                    Karta karta = talia[i];
                    talia[i] = talia[j];
                    talia[j] = karta;
                }
            }
        }

        private void Rozdaj_karty()
        {
            for (Gracz g = Gracz.North; talia.Count > 0; )
            {
                gracz[(int)g].Add(talia.Last());
                talia.RemoveAt(talia.Count - 1);
                if (g < Gracz.South)
                    g++;
                else
                    g = Gracz.West;
            }
            foreach (List<Karta> ręka in gracz)
                ręka.Sort(delegate(Karta x, Karta y)
                {
                    return (x.kol > y.kol || (x.kol == y.kol && x.wys > y.wys)) ? -1 : 1;
                });
        }

        private void Pokaż_rozkład()
        {
            Układ(gracz[0], new Label[] { piki_w, kiery_w, kara_w, trefle_w });
            Układ(gracz[1], new Label[] { piki_n, kiery_n, kara_n, trefle_n });
            Układ(gracz[2], new Label[] { piki_e, kiery_e, kara_e, trefle_e });
            Układ(gracz[3], new Label[] { piki_s, kiery_s, kara_s, trefle_s });
        }

        private void Układ(List<Karta> ręka, Label[] label)
        {
            string[] SW = { "2 ", "3 ", "4 ", "5 ", "6 ","7 ", "8 ", "9 ", "10 ",
                            "W ", "D ", "K ", "A " };
            StringBuilder[] s = { new StringBuilder(), new StringBuilder(),
                                  new StringBuilder(), new StringBuilder() };
            foreach (Karta karta in ręka)
                s[(int)karta.kol].Append(SW[(int)karta.wys]);
            for (int k = 0; k < 4; k++)
                label[k].Text = s[k].ToString();
        }

        private void rozdajToolStripMenuItem_Click(object sender, EventArgs e)
        {
            foreach (List<Karta> ręka in gracz)
            {
                talia.AddRange(ręka);
                ręka.Clear();
            }
            Tasuj_karty();
            Rozdaj_karty();
            Pokaż_rozkład();
        }

        private void zakończToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        List<Karta> talia = null;
        List<Karta>[] gracz = { null, null, null, null };
        Random gen;
    }
}

A oto przykładowy wynik wykonania powyższego programu:


Opracowanie przykładu: kwiecień 2020