Hledání min ve WPF

Dalším programem, který vznikl v rámci procvičování algoritmů s maticemi, je klasická hra Hledání min, tentokrát ve WPF. 

 

Hra zobrazuje minové pole o zvolených rozměrech, ve kterém je náhodně rozmístěn zvolený počet min. Pole se odkrývají kliknutím levým tlačítkem myši, pravým se na ně umísťuje (nebo odebírá) značka, jejíž přítomnost znemožňuje odkrytí pole. Okolo každé miny jsou pole s čísly, jež udávají počet min v bezprostřední blízkosti. Po kliknutí na pole s číslem se toto číslo zobrazí. Po odkrytí prázdného pole se odkryje nejen toto pole, ale i všechna s ním sousedící prázdná pole. Při kliknutí na minu hra končí prohrou a odkryjí se všechna zbývající pole.

Hledání min ve WPF

V projektu byly vytvořeny dvě uživatelské komponenty (UserControl), z nichž jedna představuje celé minové pole (MinovePole) a druhá jedno jeho políčko (Pole). Nejprve tedy začneme elementárním prvkem Pole.

 

Pole

V XAMLčásti komponenty Pole budou vytvořeny pouze dvě komponenty: Rectangle a TextBlock. Obdélník, roztažený přes celou plochu (s odsazením 2 pixelů), je černě orámovaný a ve výchozím stavu má světle šedé pozadí. TextBlock je pak nejprve prázdný a bude použit pro vypsání čísla s hodnotou pole, nebo značky. 

<Grid x:Name="grdMian" MouseLeftButtonDown="grdMian_MouseLeftButtonDown" Cursor="Hand" MouseRightButtonDown="grdMian_MouseRightButtonDown">
    <Rectangle x:Name="recPole" Margin="2" Stroke="Black" Fill="#FFECECEC"  />
    <TextBlock x:Name="txbHodnota" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>

V C# části Pole pak vytvoříme vlastnosti, určující o jaký typ pole se jedná (celé číslo, z něhož se budou používat pouze hodnoty 0-9), identifikátor, zda-li je pole již odkryto, identifikátor oznamující, zdali je na poli umístěna značka (Oznaceno) a událost pro oznámení, že došlo k odkrytí pole. 

public int typ = 0;
public int Typ                    // Typ pole (0 - nic, 1-8 - číslo, 9 - mina)
{
    get { return typ; } 
    set { if (value >= 0 && value <= 9) typ = value; }
} 
 
public bool Odkryto { get; set; } // Příznak, bylo-li pole již otočeno nebo je zatím skryto
 
public bool Oznaceno
{
    get { return txbHodnota.Text == "X"; }
    private set {
        if (String.IsNullOrEmpty(txbHodnota.Text) || Oznaceno) // Je-li pole otočeno, nelze jej označovat
            txbHodnota.Text = value ? "X" : "";
    }
}
public event EventHandler Odkryti; // Událost vyvolaná při otočení pole kliknutím na něj

Hodnoty vlastnosti Typ tedy budou mít následující význam: 0 bude prázdné pole, 1-8 bude pole s číslem oznamující počet min v jeho bezprostřední blízkosti a 9 bude hodnota pro minu. Vlastnost Oznacen je pak z venčí určena pouze pro čtení a její hodnota je přímo odvozena od znaku, který je v políčku zobrazen - je-li to X, pak vlastnost vrací true, jinak false. Při zápisu, jež je možný jen zevnitř této komponenty a pouze pokud karta není odkryta (není na ní uvedena žádná hodnota kromě značky), se pak podle stavu zapisované hodnoty umístí do popisku tbxHodnota buď X nebo prázdný řetězec. 

Další částí kódu je metoda obsluhující událost při kliknutí na Pole levým tlačítkem myši. Ta je zachytávána hlavním gridem na pozadí, díky čemuž zachytává kliknutí na kteroukoli komponentu v něm umístěnou. Po kliknutí se tedy nejprve otestuje, není-li již pole odkryto nebo označeno (v takovém případě by se nestalo nic). Pokud ne, dojde k odkrytí pole zavoláním metody Odkryj a oznámení této skutečnosti ostatním komponentám spuštěním události Odkryti

private void grdMian_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (Odkryto) return;            // Je-li pole již otočeno, pak se znovu otáčet nebude
    if (Oznaceno) return;           // Je-li pole označeno jako mina, nejde otočit, dokud se označení nezruší
 
    Odkryj();                         // Odkrytí pole
    if (Odkryti != null)            // Zavolání události oznamující otočení pole uživatelem
        Odkryti(this, EventArgs.Empty);
}

Kliknutí pravým tlačítkem myši pak pouze dochází k označení pole, což lze díky set kódu vlastnosti Oznaceno provést pouze její negací. 

private void grdMian_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    Oznaceno = !Oznaceno;
}

Metoda pro odkrytí pole Odkryj nejprve otestuje, není-li již pole odkryto, a pokud ano, dále nepokračuje. V opačném případě aktivuje příznak odkrytí, do textu pole vloží číslo jejího typu (0-9), změní kurzor pro pole z ručičky na šipku (ta značí, že se na něj již nedá znovu kliknout) a nakonec nastaví barvu pozadí obdélníka vyznačujícího pole v závislosti na jeho typu (prázdné pole šedivá, číslo světle modrá a mina černá, takže text na ní nebude vidět). 

public void Odkryj()
{
    if (Odkryto) return;              // Je-li pole již otočeno, pak metoda znovu neproběhne
    Odkryto = true;                   // Nastavení příznaku odkrytí
  
    txbHodnota.Text = Typ.ToString(); // Vypsání hodnoty pole (0-9)
    grdMian.Cursor = Cursors.Arrow;   // Kurzor přepnut na šipku - na otočené pole se již znovu nekliká
    Color barva;                      // Pomocná proměnná pro určení nové barvy pozadí
    switch (Typ)                      // Podle typu pole zvolí novou barvu pro jeho pozadí
    {
        case 9: barva = Colors.Black; break;   // Miny budou černé (číslo 9 na nich tak nebude vidět)
        case 0: barva = Colors.Silver; break;  // Prázdná pole budou šedivá
        default: barva = Colors.Aqua; break;   // Ostatní (pole s čísly) budou světle modrá
    }
    recPole.Fill = new SolidColorBrush(barva); // Nastavení pozadí dle zvolené barvy
}

 

Minové pole

Komponenta MinovePole v XAML části nemá vůbec nic, pouze hlavní grid je pojmenován grdMain. Vše podstatné se tu totiž děje v C# části kódu. Nejprve nadefinujeme datové položky (fields), vlastnosti (properties) a události (events). 

// Datové položky
Pole[,] miny;                        // Pomocná matice pro ukládání odkazů na instance jednotlivých Polí
static Random random = new Random(); // Objekt pro generování náhodných hodnot
// Vlastnosti minového pole
public int Sirka { get; set; }
public int Vyska { get; set; }
public int PocetMin { get; set; }
// Událost oznamující konec hry 
public enum TypKonce { Vyhra, Prohra }         // Výčet pro určení typu konce hry
public delegate void Konec(TypKonce typKonce); // Delegát určující typ (strukturu hlavičky) události
public event Konec KonecHry;                   // Událost 

Výchozí hodnoty vlastnostem (Sirka, Vyska a PocetMin) nastavíme v konstruktoru, např. na 10 (všem třem).

Vytvoření obsahu komponenty a přípravu dat má pak na starosti metoda VytvorPole. Ta nejprve vymaže všechny případné stávající prvky v hlavním gridu (např. před začátkem nové hry) a pak celou jeho strukturu vytvoří znovu - sloupce a řádky mřížky (gridu) a do každé z takto vzniklých buněk instanci komponenty Pole. Reference na tato pole jsou kromě gridu uloženy také do pomocného 2D pole miny. Díky tomu k nim pak bude možné efektivně přistupovat pouze na základě jejich indexů. Toho se využije hned po té, kde jsou do náhodně zvolených polí rozmístěny miny (typ polí je nastaven na 9) v příslušném počtu (hlídá se, aby se mina nedostala 2x do stejných souřadnic).

Na závěr metody pak proběhne výpočet hodnot pro pole obsahující čísla. Za tímto účelem jsou zkontrolována všechna jednotlivá pole a není-li na nich již mina, jsou zkontrolováni všichni jeho sousedé (o jeden doleva a nahoru až o jeden doprava a dolů) a za každou minu, která se tam nachází je hodnota pole (typ) zvýšena o +1.  

public void VytvorPole()
{
    // Příprava gridu
    grdMain.Children.Clear();             // Vymaže všechny komponenty gridu 
    grdMain.RowDefinitions.Clear();       // Zruší všechny řádky gridu
    grdMain.ColumnDefinitions.Clear();    // Zruší všechny sloupce gridu
 
    for (int i = 0; i < Sirka; i++)       // Vytvoření sloupců gridu
        grdMain.ColumnDefinitions.Add(new ColumnDefinition());
    for (int i = 0; i < Vyska; i++)       // Vytvoření řádků gridu
        grdMain.RowDefinitions.Add(new RowDefinition());
 
    // Vytvoření a umístění polí
    miny = new Pole[Vyska, Sirka];        // Pomocné matice s odkazy na objekty typu Pole
    for (int i = 0; i < Vyska; i++)
        for (int j = 0; j < Sirka; j++)
        {
            var pole = new Pole();        // Vytvoření nové instance komponenty Pole
            Grid.SetColumn(pole, j);      // Umístění do sloupce
            Grid.SetRow(pole, i);         // Umístění do řádku
            pole.Odkryti += pole_Odkryti; // Událost při otáčení pole
            grdMain.Children.Add(pole);   // Vložení Pole do gridu
            miny[i, j] = pole;            // Uložení odkazu na Pole do pomocné matice
        }
 
    // Rozmístění min
    for (int k = 0; k < PocetMin; k++)
    {
        int i = random.Next(Vyska);     // Náhodný řádek
        int j = random.Next(Sirka);     // Náhodný sloupec
        while (miny[i, j].Typ == 9)     // Je-li tam již jiná mina, vybrat jiné souřadnice
        {
            i = random.Next(Vyska);
            j = random.Next(Sirka);
        }
        miny[i, j].Typ = 9;             // Umístění miny (9)
    }
 
    // Očíslování položek sousedících s minami
    for (int i = 0; i < Vyska; i++)                            // Pro všechny řádky
        for (int j = 0; j < Sirka; j++)                        // v každém sloupci
            if (miny[i, j].Typ != 9)                           // není-li zde mina
                for (int x = -1; x <= 1; x++)                  // projít od pole o 1 vlevo do o 1 vpravo
                    for (int y = -1; y <= 1; y++)              // a také pro o 1 řádek nad až po 1 řádek pod
                        if (j + x >= 0 && i + y >= 0 &&        // nepřekročili-li se levé hranice plochy
                            j + x < Sirka && i + y < Vyska &&  // ani pravé hranice plochy
                            (x != 0 || y != 0))                // a aktuálně řešená buňka se nepočítá 
                            if (miny[i + y, j + x].Typ == 9)   // je-li v sousedící buňce mina
                                miny[i, j].Typ++;              // zvýšit číslo v této buňky o +1
}

Další metoda obsluhuje událost při odkrytí jednoho pole (po klinutí na něj). Je tedy reakcí na událost Odkryti ve třídě Pole. Metoda byla této události přidělena u všech polí při jejich vytvoření v předchozí metodě (VytvorPole). Pole se po kliknutí na něj automaticky odkryje, avšak veškeré další úkony je třeba vyřešit zde. U prázdných polí (typ = 0) by se měla odkrýt všechna pole stejného typu v souvislé ploše, jíž je toto součástí. O to se postará metoda OdkryjPoleASousedy, jež bude popsána později. Dále je třeba reagovat na odkrytí pole s minou, což znamená okamžitý konec hry (prohru) a odkrytí všech zbývajících polí. Na závěr je také otestováno, jestli již nejsou otočena všechna pole (kromě min), což by také znamenalo konec hry, tentokrát však výhru

void pole_Odkryti(object sender, EventArgs e)
{
    Pole pole = (Pole)sender;  // Uložení přetypovaného odesílatele (Pole) do pomocné proměnné
    if (pole.Typ == 0)         // Kliklo se na prázdné pole - otočit všechny sousední pázdná pole
    {
        pole.Odkryto = false;  // Aby fungovala následující rekurzivní metoda, je třeba nastavit toto
        OdkryjPoleASousedy(Grid.GetRow(pole), Grid.GetColumn(pole)); // Odkrytí všech sousedících prázdných polí
    }
    else
        if (pole.Typ == 9)  // Kliklo se na minu - otočit všechna neotočená pole
        {    
            for (int i = 0; i < Vyska; i++)
                for (int j = 0; j < Sirka; j++)
                    miny[i, j].Odkryj();  // Otočení neotočeného pole
            if (KonecHry != null)         // Ukončení hry prohrou
                KonecHry(TypKonce.Prohra);
            return;                       // Ukončení této metody 
        } // Při kliknutí na číslo se toto otočí samo a netřeba na nic reagovat
 
    // Kontrola výhry - vše, krom min je otočeno
    for (int i = 0; i < Vyska; i++)
        for (int j = 0; j < Sirka; j++)
            if (!miny[i, j].Odkryto && miny[i, j].Typ != 9)
                return;    // Nalezeno něco neotočeného, co není min => ukončit hledání
    if (KonecHry != null)  // Nic neotočeného nenalezeno => konec hry
        KonecHry(TypKonce.Vyhra);
}

Poslední metoda, která je volána z té předchozí, má za úkol odkrýt prázdné pole (typ = 0) na zadaných souřadnicích a spolu s ním i všechna prázdná pole nacházející se ve stejné souvislé ploše prázdných polí (ortogonálně sousedící s prvním odkrývaným polem). To je zde realizováno rekurzivním voláním sebe sama pro všechny sousedy (nahoře, dole, vlevo a vpravo), což spustí totéž i pro sousedy těchto sousedů atd. až po meze vytyčené hrací plochou nebo jiným typem pole.  

void OdkryjPoleASousedy(int i, int j)
{
    // Odkrytí všech sousedních prázdných polí algoritmem semínkového vyplňování 
    if (j >= 0 && i >= 0 && j < Sirka && i < Vyska && !miny[i, j].Odkryto)  // Jsou-li zadané souřadnice v ploše minového pole a pole není odkryto
        if (miny[i, j].Typ == 0)                           // Je-li v daném poli prázdno 
        {
            miny[i, j].Odkryj();                           // Odkrytí pole
            // Odkrytí sousedů
            for (int x = -1; x <= 1; x++)                  // Projít od pole o 1 vlevo do o 1 vpravo
                for (int y = -1; y <= 1; y++)              // a také pro o 1 řádek nad až po 1 řádek pod
                    OdkryjPoleASousedy(i + y, j + x);      // odkrýt toto pole i jeho sousedy 
        }
        else
            if (miny[i, j].Typ > 0 && miny[i, j].Typ < 9)  // Není-li pole prázdné, odkrýt jej ale jeho sousedy již ne
                miny[i, j].Odkryj();                       // Odkrytí pole
}

Tento princip využívá tzv. semínkový algoritmus. Na první pole se zasadí semínko, z něho vyroste strom, ze kterého opadají semínka na okolní pole, kde opět vyrostou stromy atd. až k hranicím plodné půdy).

 

Hlavní okno

Do hlavního okna aplikace (MainWindow) se v XAMLu umístí pouze jediná komponenta - MinovePole, vytvořena v předchozí části (projekt je před tím potřeba zkompilovat - Build). U ní je pak především třeba reagovat na událost KonecHry, oznamující ukončení hry a typ tohoto konce (výhra/prohra). 

<local:MinovePole x:Name="usrMinovePole" Margin="10" KonecHry="usrMinovePole_KonecHry"/>

V C# části kódu přidáme do konstruktoru zavolání metody VytvorPole této komponenty (až po inicializaci komponent). Ta je volána takto externě, jelikož její spuštění v konstruktoru komponenty samotné by bez dalších opatření zkomplikovalo její zobrazení v době návrhu designu. 

usrMinovePole.VytvorPole();

Metoda obsluhující událost konce hry pak může vypadat třeba následovně. Pro jednoduchost pouze prostřednictvím dialogu oznámí, jestli hráč vyhrál nebo prohrál, a po uzavření dialogu je zahájena nová hra. 

private void usrMinovePole_KonecHry(MinovePole.TypKonce typKonce)
{
    if (typKonce == MinovePole.TypKonce.Vyhra)
        MessageBox.Show("Vyhrál jsi!", "Konec hry");
    else if (typKonce == MinovePole.TypKonce.Prohra)
        MessageBox.Show("Prohrál jsi...", "Konec hry");
    usrMinovePole.VytvorPole();    // Zahájení nové hry
}

Vytvoření jiné, efektnější reakce na obě možná zakončení hry samozřejmě nic nebrání. Stejně jakož i lepšímu grafickému zpracování polí (např. místo barevných ploch by mohly být zobrazovány obrázky). Při otáčení polí by také mohly být přehrávány zvuky, ve hře by bylo i vhodné okno pro nastavení parametrů hry (rozměry minového pole a počet min), přehled skóre výsledků apod. Základní algoritmus však může vypadat právě tak, jak zde byl nastíněn...

 

Mina

 

 

on 10 březen 2014