× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Delphi v příkladech - 3. díl: Hledání min

[ http://programujte.com/profil/2893-petr-lepcio/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/118-zdenek-lehocky/ ]Google [ ?rel=author ]       27. 7. 2007       30 422×

Dnes si vytvoříme základ hledání min. Ukážeme si základy práce s třídami (class) a práci s dynamickými poli a jejich použití.

Hledání min je jednoduchá hra, ve které má hráč k dispozici políčka, na kterých je rozmístěn určitý počet min a která postupně odkrývá. Pokud narazí na pole s minou, prohrál. V opačném případě se na políčku zobrazí číslo (0-8), které udává, kolik min se v okolí nachází. Políčka, o kterých si myslí, že skrývají minu, si může označit kliknutím pravým tlačítkem myši. Cílem hry je správně označit všechna políčka s minami.

Výsledný program [ http://programujte.com/storage/200712071759_Miny.exe ]

Struktura programu

Na první pohled je zřejmé, že pro miny a praporky si nevystačíme s komponentou jako label, která umí zobrazovat pouze text. Dalo by se sice vystačit i s nimi, ale výsledek by byl nepřehledný, matoucí a málo efektivní. Proto použijeme komponentu image (obrázek). Ta ale zabírá v paměti výrazně více místa, a proto si nemůžeme dovolit jen tak vytvořit pole o velikosti 20 × 30 obrázků. Místo toho si vystačíme pouze se dvěma. Do prvního si uložíme obrázek, na kterém budou všechny možné varianty políček, která budeme rozkopírovávat do druhého obrázku. I přesto se ale nevyhneme použití pole. V něm ale namísto celých komponent uložíme pouze několik důležitých informací. Jelikož také budeme chtít mít možnost nechat uživatele vybrat si rozměry hracího pole, musíme s tím počítat už nyní (věnovat se tomu ale budeme až někdy příště). Proto si nadeklarujeme konstanty sirka, vyska, velikost a pocetmin. Tyto konstanty budou globální, a proto je deklarujeme ještě před částí type. Pro začátek jim můžeme nechat třeba tyto hodnoty:


const sirka = 30;
      vyska = 20;
      velikost = 16;  //označuje velikost jednoho políčka
      pocetmin = 99;

type
  ...

Pro ukládání informací o minách si vytvoříme vlastní strukturovaný datový typ, který pojmenujeme TPolicko. Mohli bychom použít záznam (record), ale ten nemůže obsahovat vlastní procedury a funkce. To umožňuje třída (class), která obsahuje objekty, kterými mohou být i procedury a funkce. Procedury a funkce třídy se obecně nazývají metody.

  TPolicko = class
      x,y:integer;        //souřadnice políčka (x,y)
      mina,               //zda-li políčko obsahuje minu
      kliknuto,           //zda-li už je políčko odkryto
      oznaceno:boolean;   //zda-li je označeno praporkem jako mina
    end;

Samozřejmě využijeme možnost vytvořit si vlastní metodu – vytvoříme funkci MinOkolo, která bude vracet počet min v okolí a v případě, že se na políčku nachází mina, bude vracet hodnotu 9. Zatím ji ale vytvářet nebudeme, protože pro ni ještě nemáme vše připravené (pokud ji i přesto deklarujeme, tak ji zatím necháme jako poznámku).

Proměnné typu třída se nazývají instance a na rozdíl od běžných proměnných se musí inicializovat (pro jejich použití nestačí jen deklarace). K inicializaci se používají speciální metody, které se nazývají konstruktory a které se starají o vytvoření všech objektů třídy (případně nastavení počáteční hodnoty). Naopak pro uvolnění z paměti se používají metody zvané destruktory. Každá třída zdědila po základní třídě TObject konstruktor Create a destruktor Destroy (lepší je volat jej prostřednictvím metody Free, kterou také automaticky obsahují všechny třídy). Často si ale s těmito základními metodami nevystačíme, a proto si vytvoříme vlastní. Deklarují se stejně jako obyčejné procedury, pouze se změní klíčové slovo procedure na constructor. Náš konstruktor nastaví hodnoty x a y podle zadaných parametrů a ostatní hodnoty na false.

//deklarace
  TPolicko = class
      x,y:integer;
      mina,kliknuto,oznaceno:boolean;
      constructor Init(x,y:byte);
      //function MinOkolo:byte; //zatím pouze poznámka!
    end;
...
//definice (v části implementation)
constructor TPolicko.Init(x,y:byte);
begin
  self.x := x;
  self.y := y;
  self.Mina := false;
  self.Kliknuto := false;
  self.Oznaceno := false;
end;

Abychom bezpečně rozlišili objekty x a y od parametrů se stejným jménem (které je překryjí), použili jsme proměnnou self. Definici jsme umístili do stejného místa jako u metod formuláře – v implementation. Všimněte si, že místo obvyklého TForm1 jsme použili TPolicko.Init, protože tato metoda patří do třídy TPolicko.

Komponenty

Na formulář přidáme 2 komponenty Image (záložka Additional). Do vlastnosti Picture u Image1 nastavíme pomocí Object Inspectoru zdrojový obrázek. Můžete použít třeba tento – pokud si chcete vytvořit svůj, tak dodržte rozložení políček na obrázku. Jelikož Delphi dokáže některé metody (grafická práce s obrázkem), které použijeme, použít pouze na bitmapy, musí být náš obrázek také ve formátu BMP.

Dále přidáme ještě jeden label, který umístíme do levého horního rohu a který bude zobrazovat počet doposud neoznačených min.

Dynamické pole

Nyní si vytvoříme dvourozměrné dynamické pole TPolicek. Od „obyčejných“ statických polí se liší tím, že při deklaraci není určena jejich velikost. Tu lze určit a měnit v průběhu programu. Do části type napíšeme:

type
  TPolicko = class
      ...
    end;
  TPole = array of array of TPolicko; //pole je dvourozměrné

A do části var programu (pod Form1:TForm1)

  Pole:TPole;

Musíme se také postarat o nastavení délky pole. To se děje použitím metody SetLength(DynamickePole,velikost). U dvojrozměrných polí může mít každé „podpole“ různou velikost, poté se musí nastavovat pro každé zvlášť (SetLength(DynamickePole[i],velikost);), nebo mohou mít všechny stejnou a nastavit se najednou (SetLength(DynamickePole,velikost,velikost_podpoli);). Indexy dynamických polí začínají vždy nulou a končí velikostí −1. Velikost pole lze zjistit funkcí Length(Pole).

Jelikož zatím uživatel nebude mít možnost měnit rozsah pole během běhu programu, nastavíme ji pouze jednou při spuštění – v metodě onCreate formuláře. Pro nastavení ostatních věcí na začátku hry si ale vytvoříme proceduru NovaHra.

procedure TForm1.FormCreate(Sender: TObject);
  var x,y:byte;
begin
  randomize; //spuštění generátoru náhodných čísel
  SetLength(Pole,sirka,vyska); //nastaveni velikosti pole
  for x := 0 to sirka - 1 do
    for y := 0 to vyska - 1 do
      Pole[x,y] :=  TPolicko.Init(x,y);  //inicializace prvků v poli
  NovaHra;  //připravení nové hry
end;

Na začátku nové hry musíme samozřejmě vymazat staré hodnoty – nastavit znovu hodnoty objektů na false. Dále se musíme postarat o přizpůsobení velikosti Image2 a připravit na něj čisté hrací pole. Komponenty Image (a některé další) mají svoje plátno (tzv. Canvas), jež se skládá z grafických bodů, které mají svoji barvu (barva je typu TColor, který je definován v jednotce Graphics, a je tvořena výčtem jednotlivých barev). Tu lze několika metodami měnit – buď přímo (Image1.Canvas.Pixels[x,y] := clBlue;), nebo pomocí několika složitějších procedur.

  • vytvořit čáru z aktuálního bodu (LineTo(x,y);) – pokud chcete pouze změnit výchozí pozici a nevytvářet čáru, použijte proceduru MoveTo(x,y)
  • okopírováním části jiného canvasu –
    CopyRect(CilovyRECT,vychoziCanvas,vychoziRECT); – vytvořit geometrický útvar (např. obdélník)

A ještě budeme potřebovat globální proměnné zbyvamin (která udává počet zbývajících min) a konechry (aby při ukončení hry nemohl hráč dále hrát) a samozřejmě nesmíme zapomenout na rozložení min po hracím poli.

type
  ...
  TForm1 = class(TForm)
    ...
  public
    zbyvamin:integer;
    konechry:boolean;
  end;
...
procedure TForm1.NovaHra;
  var x,y,oznaceno_min:byte; //řídící proměnná cyklu
begin
  for x := 0 to sirka - 1 do    //vynulování starých hodnot
    for y := 0 to vyska - 1 do
      begin
        Pole[x,y].kliknuto := false;
        Pole[x,y].mina := false;
        Pole[x,y].oznaceno := false;
      end;
  Label1.Caption := 'Zbývá min: '+inttostr(pocetmin);  //nastavení informačního labelu
  Image2.Width := sirka*velikost-1;   //nastavení rozměrů obrázku v závislosti na velikosti pole
  Image2.Height := vyska*velikost-1;
  for x := 0 to sirka - 1 do
    for y := 0 to vyska - 1 do
      Image2.Canvas.CopyRect(Rect(velikost*x-1,velikost*y-1,velikost*(x+1)-1,velikost*(y+1)-1),Image1.Canvas,Rect(12*velikost,0,13*velikost,velikost));
     //rozkopírování políček z obrázku Image1 na Image2
  for oznaceno_min := 1 to pocetmin do
    begin
      repeat       //'náhodné' rozložení min v poli
        x := random(sirka);
        y := random(vyska);
      until not (Pole[x,y].mina);  //kontrola, aby se miny neopakovaly
      Pole[x,y].mina := true;
    end;
  zbyvamin:=pocetmin;  
  konechry := false;  //výchozí nastavení proměnných
end;

Když už políčka inicializujeme, měli bychom se postarat i o jejich uvolnění z paměti.

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var x,y:byte;
begin
  for x := 0 to sirka - 1 do
    for y := 0 to vyska - 1 do
      pole[x,y].Free;
end;

Když už máme připravené pole, můžeme naprogramovat i již výše zmíněnou funkci MinOkolo.

function TPolicko.MinOkolo:byte;
  var min_okolo:byte;
begin
  if Pole[x,y].mina then min_okolo := 9 else  //pokud je zde mina, výsledkem je 9
    begin
      min_okolo := 0;  //začínáme nulou
      if (self.x > 0) and (Pole[self.x-1,self.y].Mina) then inc(min_okolo);
      if (self.y > 0) and (Pole[self.x,self.y-1].Mina) then inc(min_okolo);
      if (self.x < sirka-1) and (Pole[self.x+1,self.y].Mina) then inc(min_okolo);
      if (self.y < vyska-1) and (Pole[self.x,self.y+1].Mina) then inc(min_okolo);
      if (self.x > 0) and (self.y > 0) and (Pole[self.x-1,self.y-1].Mina) then inc(min_okolo);
      if (self.x > 0) and (self.y < vyska-1) and (Pole[self.x-1,self.y+1].Mina) then inc(min_okolo);
      if (self.x < sirka-1) and (self.y > 0) and (Pole[self.x+1,self.y-1].Mina) then inc(min_okolo);
      if (self.x < sirka-1) and (self.y < vyska-1) and (Pole[self.x+1,self.y+1].Mina) then inc(min_okolo);
    end; //a za každou minu okolo přičteme 1 (inc(x) zvyšuje hodnotu x o 1)
  Result := min_okolo;  //vrácení výsledku
end;

Tady si musíme dát pozor na rozměr pole. Pokud bychom se snažili překročit rozsah pole, skončí náš program chybou. Mohlo by se zdát, že při vyhodnocování podmínek by mohla nastat chyba, jenže nenastane. Je to dáno způsobem vyhodnocování podmínek – pokud jsou 2 výrazy spojeny pomocí spojky and a první z nich má hodnotu false, je jisté, že celý výraz bude také false, a proto se nemusí vyhodnocovat 2. část výrazu.

Pro vyhodnocování klikání uživatele použijeme metodu OnMouseDown Image2. Musíme totiž rozlišit, které tlačítko myši bylo zmáčknuto. Pokud pravé, bude se měnit označení praporkem na políčku, pokud levé, musí se zobrazit příslušný obrázek a pokud je na něm mina, ukončit hru, nebo pokud je na něm 0, tak zobrazit i sousední políčka.

procedure TForm1.Image2MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var sx,sy,minkolem:byte;
begin
  sx:= x div velikost;  
  sy:= y div velikost;  //zjištění souřadnic zmáčknutého políčka
  if (not Pole[sx,sy].kliknuto) and (not konechry) then
      //musíme zkontrolovat, zda-li již není políčko označené a zda-li není konec hry
    begin
      if (Button = mbRight) then  //rozlišení tlačítka
        begin
          if not (Pole[sx,sy].Oznaceno) then
            begin
              Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(11*velikost,0,12*velikost,velikost));
              zbyvamin := zbyvamin-1;
               //označení praporkem
            end
          else
            begin
              Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(12*velikost,0,13*velikost,velikost));
              inc(zbyvamin);  //odstranění praporku
            end;
          Pole[sx,sy].Oznaceno := not Pole[sx,sy].Oznaceno;   //změna příslušné hodnoty
          Label1.Caption := 'Zbývá min: '+inttostr(zbyvamin); //úprava informace o zbývajících minách
          if zbyvamin = 0 then   //kontrola vítězství
            KontrolaMin;
        end
      else
        if (Button = mbLeft) and (not Pole[sx,sy].oznaceno) then  //nelze klikat na políčka s praporkem
          begin
            minkolem := Pole[sx,sy].MinOkolo;  //využití fce na zjištění min v okolí
            Image2.Canvas.CopyRect(Rect(sx*velikost-1,sy*velikost-1,(sx+1)*velikost-1,(sy+1)*velikost-1),Image1.Canvas,Rect(minkolem*velikost,0,(minkolem+1)*velikost,velikost));
            (*jelikož máme příslušné obrázky seřazeny za sebou podle rostoucí hodnoty, kterou vrací funkce MinOkolo,
              stačí správně posunout vyřezávaný obrázek *)
            Pole[sx,sy].kliknuto := true;  //označení, že se zde už hrálo
            case minkolem of
              0: begin  //zmáčknutí sousedních tlačítek
                   if (sx > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,sy*velikost);
                   if (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],sx*velikost,(sy-1)*velikost);
                   if (sx < sirka-1) then Image2.OnMouseDown(Image2,mbleft,[],(sx+1)*velikost,sy*velikost);
                   if (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],sx*velikost,(sy+1)*velikost);
                   if (sx > 0) and (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,(sy-1)*velikost);
                   if (sx > 0) and (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],(sx-1)*velikost,(sy+1)*velikost);
                   if (sx < sirka-1) and (sy > 0) then Image2.OnMouseDown(Image2,mbleft,[],(sx+1)*velikost,(sy-1)*velikost);
                   if (sx < sirka-1) and (sy < vyska-1) then Image2.OnMouseDown(Image2,mbleft,[],(1+sx)*velikost,(sy+1)*velikost);
                 end;
              9: begin  //konec hry a zobrazení pozic min
                   konechry := true;
                   ukazminy;
                 end;
            end;
          end;
    end;
end;

V programu jsme ale použili 2 procedury (kontrolamin a ukazminy), které musíme samozřejmě také naprogramovat. Miny je totiž nutno zkontrolovat, zda-li jsou umístěny na správných pozicích.

procedure TForm1.UkazMiny;
var x,y:byte;
begin
  for x := 0 to sirka - 1 do
    for y := 0 to vyska - 1 do
      begin  //zajímají nás 2 situace - políčko je označené a nemělo by být, nebo není označené a mělo by být
        if (Pole[x,y].oznaceno) and (not Pole[x,y].mina) then    //1. možnost, zobrazení červeného křížku
          Image2.Canvas.CopyRect(Rect(x*velikost-1,y*velikost-1,(x+1)*velikost-1,(y+1)*velikost-1),Image1.Canvas,Rect(10*velikost,0,11*velikost,velikost))
        else
          if (not Pole[x,y].oznaceno) and (Pole[x,y].mina) then  //2. možnost, zobrazení miny
            Image2.Canvas.CopyRect(Rect(x*velikost-1,y*velikost-1,(x+1)*velikost-1,(y+1)*velikost-1),Image1.Canvas,Rect(9*velikost,0,10*velikost,velikost))
      end;
end;
procedure TForm1.KontrolaMin;
var
  x,y:byte;
  ukoncit:boolean;
begin
  ukoncit := true;  //výchozí hodnota
  for x := 0 to sirka - 1 do
    for y := 0 to vyska - 1 do
      begin
        if pole[x,y].Mina and (not pole[x,y].oznaceno)
          then
            ukoncit := false;  //pokud nějaká mina není v pořádku, změníme na false
      end;
  if ukoncit then // pokud je ukoncit stále true, jsou všecny miny označeny správně
    begin
      for x := 0 to sirka - 1 do   //zobrazení nezobrazených políček
        for y := 0 to vyska - 1 do
          if not pole[x,y].mina then Image2.OnMouseDown(Image2,mbleft,[],x*velikost,y*velikost);
      konechry := true;   //konechry
      //dotaz na novou hru
      if messagedlg('Blahopřeji, vyhráli jste,'+#13+'chcete spustit novou hru?',mtCustom,[mbYes,mbNo],0) = mryes then Novahra;
    end;
end;

Na konci procedury jsme použili funkci MessageDlg vyvolávající zprávu, která má ale oproti jednoduché zprávě Showmessage daleko více možností. Má tyto parametry:

  • řetězec zobrazené zprávy
  • typ zprávy (ikonka) – mtError, mtInformation, mtWarning, mtCustom
  • tlačítka – množina, která může obsahovat např. mbYes, mbNo, mbCancel, mbIgnore
  • číslo nápovědy (nemáme nápovědu → 0)
  • funkce vrací hodnotu stisknutého tlačítka (jako jména tlačítek, jen místo mb je mr → mrYes, mrNo, …), tato vrácená hodnota je typu word

Menu

Většina aplikací ve Windows s oblibou využívá menu (nabídku). Delphi podporuje 2 druhy menu – hlavní menu (main menu) zobrazené na vrchu formuláře a popup menu, které se zobrazuje po kliknutí pravým tlačítkem myši.
Nám se více hodí hlavní menu. Jeho tvorba je velmi jednoduchá. Na formulář přidáme komponentu MainMenu (ze záložky Standard) a dvojklikem na ni otevřeme okno pro editaci menu, kde je již připravený objekt typu TMenuItem. Nastavíme jeho caption na „Hra“ a vidíme, že se pod ním vytvořila prázdná položka a zároveň se připravil další prázdný objekt, obojí typu TMenuItem. Vnořenému objektu přiřadíme caption „Nová hra“. Zároveň s tím se připraví další prázdná položka. Stejným způsobem tedy vytvoříme ještě položku „Konec“ a mezi ně vložíme oddělovací čáru (nastavením captionu položky na „–“).

Všimněte si také, jak jsou tyto objekty hiearchicky řazeny v ObjectTreeView.

Po zavření editačního okna vidíme, že se příslušná nabídka připojila k formuláři (nebo můžete sami přiřadit menu formuláři – vlastnost formuláře Menu). Kliknutím na položku Hra se nabídka rozevře a zobrazí nabídku. Kliknutím na jednu z položek se vytvoří procedura obsluhující kliknutí na ni. Jejich kód je celkem jednoduchý a není na něm nic zajímavého.

procedure TForm1.Novhra1Click(Sender: TObject);
var tlacitko:word;
begin
  if not konechry
    then
      tlacitko := messagedlg('Chcete začít novou hru?',mtconfirmation,[mbyes,mbno],0);
      //dotaz na novou hru
  if (tlacitko = mryes) or konechry
    then
      novahra;
end;

procedure TForm1.Konec1Click(Sender: TObject);
begin
  close; //konec programu
end;

Závěrem

Na závěr je ještě zapořebí pohrát si s detaily – změnit ikony, Caption formluláře, pohrát si s umístěním komponent na formuláři a s jejich velikostmi (kromě Image2, o který se staráme během programu), nastavit vlastnost Image1.Visible na false, aby obrázek na formuláři „nepřekážel“ a podobné úpravy.


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2007052602-delphi-v-prikladech-3-dil-hledani-min/ ].