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.
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.
private 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...