Potapljanje ladjic

Potapljanje ladjic

Avtor: pred. M.Lokar, prenos v NAUK Alja Gligić, Saša Udir

NAVODILO NALOGE

Izdelaj kompletno aplikacijo za preprosto potezno igro POTAPLJANJE LADJIC med dvema igralcema (en igralec je računalnik).

PRAVILA IGRE

Igro igrata dva igralca. V našem primeru je eden izmed igralcev računalnik.

Vsak igralec ima igralno mrežo 10x10 kvadratkov. Hkrati ima na voljo 7 ladjic:

  • eno letalonosilko, ki zavzame 5 kvadratkov,
  • eno bojno ladjo, ki zavzame 4 kvadratke,
  • eno križarko, ki zavzame 3 kvadratke,
  • dva raketna čolna, vsak zavzame 2 kvadratka
  • in dve podmornici, ki vsaka zavzame po 1 kvadratek.

Vsak igralec postavi na svojo mrežo poljubno vse ladjice.

  • Ladjice so lahko postavljene zgolj vodoravno ali navpično.
  • Držati se mora tudi pravila, da se ladjice ne smejo dotikati (niti po diagonalah).
  • V našem primeru poenostavimo problem in se odločimo, da bomo za oba igralca naključno postavili ladje (igralec jih ne bo mogel postaviti sam).

Ko imata oba igralca postavljene vse ladjice, se igra začne.

Igralca izmenično "streljata" nasprotnikova polja. Ko napadalec "strelja" mora nasprotnik (v našem primeru računalnik) preveriti, ali je polje zadeto ali ne in to sporoči nasprotniku. Če je napadalec s potezo hkrati tudi potopil ladjo, mu mora to nasprotnik tudi sporočiti.

Zmagovalec igre je tisti, ki prvi potopi vse nasprotnikove ladje.

ZAČETEK in RAZMISLEK

Začetek

Najprej bomo ustvarili nov projekt v programu Visual Studio 2010. Poimenujemo ga npr. PotapljanjeLadjic.


Razmislek

  • Videz programa/forme
  • Hranjenje podatkov
  • Logika v ozadju - metode

Motivacija:

Omenjeno igrico lahko igramo na več spletnih straneh.

Primer: klik


Simulacija igre

Ogled simulacije igranja igre v našem programu.

Povezava do filmčka

RAZMISLEK O VIDEZU PROGRAMA

RAZMISLEK O VIDEZU PROGRAMA

GLAVNI DEL programa predstavljata dve mreži s kvadratki (velikosti 10x10). Vprašamo se, kako bi bilo mreže najlažje prikazati v programu:

  • Lahko bi mrežo predstavili z gumbi. Uporabnik bi kliknil na gumb/polje, ki ga želi potopiti.
  • Boljša ideja je, da izrisujemo mreži na platno (Panel):

    • Vsako na svoje platno (Panel).
    • Do polja, ki ga je izbral uporabnik, bomo prišli s pomočjo dogodka na platnu MouseClick in pozicije miške v tistem trenutku.
    • Odločimo se za to idejo.

Glavni del programa tako predstavljata dve platni (Panel-a). Da bo bolj pregledno, lahko vsakega postavimo v svoj GroupBox in le-tega ustrezno poimenujemo (spremenimo lastnost Text.)

Poleg dveh platnov bomo na formi prikazovali tudi različna sporočila (koliko ladij je potopljenih, kdaj je bila potopljena ladja itd.). Zato bodo poskrbele tri oznake (Label). Vsaka oznaka bo prikazovala svojo informacijo:

  • lblStatus --> Prikazujemo samo takrat, ko želimo sporočiti, da je eden od igralcev potopil ladjo.
  • lblStIgr --> Prikazujemo število potopljenih ladij na igralčevi plošči, torej število ladij, ki jih je potopil računalnik.
  • lblStRac --> Prikazujemo število ladij, ki jih je potopil igralec.

Potrebujemo samo še nek gumb, ki bo sprožil novo igro:

  • btnNovaIgra

Slika:

(DesignProjekt.png)

RAZMISLEK - HRANJENJE PODATKOV

Ko smo zadovoljni z videzom programa razmislimo, kako bomo hranili podatke v mreži oz. podatke o posameznih poljih.

Najprej premislimo, kaj moramo vedeti o posameznem polju:

  • Vedeti moramo ali je voda ali je ladja.
  • Prav tako moramo vedeti, ali je polje že zadeto.
  • Kasneje se bo izkazalo, da moramo hraniti tudi podatke o tem, ali je polje sosed ladje.

    • Zakaj? Ko bomo naključno postavljali ladje, ne smemo dveh ladij postaviti eno zraven druge. Torej vsa polja novo postavljene ladje ne smejo biti sosedje katerekoli ladje (pred postavitvijo seveda).
  • Za vsako polje, ki je del ladje, moramo vedeti kateri ladji pripada.

    • Zakaj? Ker, ko se ladja potopi, to sporočimo nasprotniku.

Kot opazimo, moramo za vsako polje vedeti kar nekaj podatkov. Lahko bi sicer imeli več kot dvodimenzionalno tabelo, a bi bilo to skrajno nepregledno.

Odločimo se, da bomo naredili dva nova razreda:

  • razred Polje,

    • Vsak objekt tega razreda bo vseboval naslednje podatke:

      • bool odkrit --> če je nasprotnik že odkril dano polje.
      • Ladja ladja --> Kazalec na ladjo, katere del je polje. Če je ladja enaka null, pomeni, da polje predstavlja vodo.
      • bool sosed --> true, če je polje sosed kake ladje.
      • bool potopljen --> true, če je ladja kateri polje pripada že potopljena.
  • razred Ladja.

    • Vsak objekt tega razreda bo vseboval naslednje podatke:

      • Polje [] polja --> tabela polj, ki pripadajo ladji.
      • bool potopljena --> true, če je ladja že potopljena oz. false, če ni.
      • int stPotopljenihPolj --> Število polj, ki so že potopljena.

S pomočjo razreda Polje tako mrežo polj hranimo kot dvodimenzionalno tabelo Polj.

  • Tabela je velika 10x10, ker je ravno toliko polj na mreži.

    • Polja [,] mrezaRacunalnik

      • Predstavlja mrežo, kjer se nahajajo ladjice računalnika. To mrežo "napada" igralec.
    • Polja [,] mrezaIgralec

      • Predstavlja mrežo, kjer se nahajajo ladjice igralca.

ZAČETEK

Kot smo že prej omenili, najprej naredimo nov projekt Windows Forms Application z imenom "PotapljanjeLadjic".

BOLJ PODROBNO:

  • Odpremo program Visual Studio.

    • V tej predstavitvi bomo delali z verzijo Visual Studio 2010 Premium.
  • Nato v zavihku File izberemo New in nato Project.
  • Odpre se nam spodnje okno:
(NovProjekt.png)
Okno - nov projekt
  • V osrednjem delu, kot se vidi na sliki, izberemo Windows Forms Application.
  • V spodnjem delu preimenujemo projekt v"PotapljanjeLadjic" (Name).

    • Po želji lahko spremenimo tudi pot, kjer se bo projekt nahajal/shranil.
  • Nato samo še potrdimo s klikom na gumb OK.

VIDEZ ("DESIGN") PROGRAMA

Potem, ko smo ustvarili nov projekt, se nam odpre projekt in se nahajamo v zavihku "Form1.cs [Design]". V tem zavihku bomo poskrbeli za videz programa.


Preden se lotimo "grajenja" forme oz. videza programa pa poskrbimo še za nekatere malenkosti.

Spremenimo besedilo naslova forme/programa

Najprej naši formi spremenimo lastnost Text. To storimo tako, da desno v oknu "Properties" poiščemo lastnost Text in vpišemo poljuben niz. V našem primeru "Potapljanje ladjic". S tem poskrbimo, da bo prikazan naslov našega programa ravno "Potapljanje ladjic" in ne "Form1".

  • Po želji preimenujemo tudi ime forme, npr. v "frmPotapljanjeLadjic".

Poskrbimo, da je v programu Visual Studio prikazan razdelek/podokno Toolbox, ker ga bomo potrebovali v prihodnjih korakih.

Če v programu Visual Studio nimamo prikazanega podokna/razdelka "Properties" ga prikažemo tako, da v zavihku View izberemo možnost Properties Window.

Podobno prikažemo tudi podokno/razdelek "Toolbox" (View/Toolbox).

VIDEZ ("DESIGN") PROGRAMA

Naša verzija programa Potapljanje ladjic ne bo omogočala povečevanja okna programa, ker ni smiselno, da je večji oz. manjši.

Na začetku v [Design]-zavihku našo formo raztegnemo na velikost 604x408 oz. v podoknu Properties spremenimo lastnost Size (za frmPotapljanjeLadjic) na 604;408.

Sedaj iz podokna Toolbox posamično in ustrezno povlečemo na formo vse želene kontrole/komponente:

  • 2 GroupBox-a,
  • znotraj vsakega GroupBox-a svoje platno (Panel),
  • 3 oznake (Label),
  • 1 gumb (Button).
(DesignProjekt.png)
Videz forme in poimenovanje kontrol

PAZITI MORAMO NA:

  • Ustrezno poimenovanje kontrol.

  • Prav tako moramo paziti, da sta obe platni (Panel-a) kvadratne oblike, ker je naša mreža polje velika 10x10.

    • V podoknu Properties spremenimo velikost platnoma (lastnost Size):

      • pnlRacunalnik--> Size --> 311; 311
      • pnlIgralec --> Size --> 150; 150

        • Platno pnlIgralec je nekoliko manjše, ker je za igralca zgolj informativno, da ve, koliko ladij mu je že potopil računalnik.
  • Oznaki lblStatus spremenimo velikost pisave na 9, ker želimo, da to oznako igralec nekoliko bolj opazi.

    • Sprememba velikosti pisave: Properties/Font/Klik na gumb "...".
    • Spomnimo se, lblStatus ima večino časa lastnost Text enako "". Kadar pa kdo izmed igralcev potopi ladjo, se ta tekst spremeni in prikaže sporočilo, da je bila potopljena ladja.

Opomba

Program bo spisan tako, da načeloma (razen začetnih nastavitev) ne bo odvisen od velikosti komponent. Če se bo programer kasneje odločil za razteg/pomanjšanje forme, tako ne bo imel večjih težav s spreminjanjem kode.

Opomba

Že na začetku projekta se dogovorimo za enotno poimenovanje kontrol oz. za oznake v imenih kontrol:

  • GroupBox --> gbo+Ime
  • Label --> lbl+Ime
  • Panel --> pnl+Ime
  • Button --> btn+Ime

PROGRAMIRANJE KODE IGRICE

Ko smo enkrat zadovoljnji s tem, kako je videti naša forma, nas čaka še najtežje delo, kaj se zgodi v ozadju. Pisanja kode se lotimo postopoma.

Razmislimo: Najprej potrebujemo narediti dve 2D tabeli polj in ju napolniti. Tukaj takoj naletimo na problem, nimamo še razreda Polje in razreda Ladja.

Projektu dodamo nov razred

To naredimo tako, da v podoknu "Solution Explorer" z desnim klikom kliknemo na naš projekt "PotapljanjeLadjic", nato izberemo "Add" in zatem "Class".

(NovRazred.png)
Dodajanje novega razreda

Nato se nam odpre okno, kjer v spodnjem delu preimenujemo objekt in potrdimo s klikom na gumb Add.

  • V našem primeru naredimo dva razreda:

    • Polje
    • Ladja

RAZRED Polje

Spomnimo se, kaj potrebujemo vedeti o polju:

  • ali je odkrit, ali ni,
  • ali je sosed kakšne ladje,
  • ali je voda, ali ni,
  • če je polje del ladje, ali je ta ladja potopljena.

Razred polje bo imel tako štiri privatne razredne spremenljivke:

  • bool odkrit:

    • true, če je polje že odkrito oz. false, če še ni.
  • Ladja ladja:

    • null, če polje predstavlja vodo oz. če ni null, polje predstavlja del ladje.
    • Trenutno nam manjka še razred Ladja.
  • bool sosed:

    • true, če je polje sosed kakšni ladji oz. false, če ni.
  • bool potopljen:

    • true, če je polje del ladje in je ta ladja že potopljena. Če je polje voda ali je polje del ladje, ki še ni v celoti potopljena, je false.

Konstruktor:

Naredili bomo konstruktor čisto po meri glede na naš projekt Potapljanje ladjic, ki na začetku napolni mrežo 10x10 s samimi neodkritimi polji, ki predstavljajo vodo. Šele kasneje pa spreminjamo dana polja, glede na potek igre.

Torej so na začetku vsa bool polja false, spremenljivka ladja pa null (ker polje predstavlja vodo).

Koda:

        public Polje()
        {
            this.ladja = null; //Na začetku vsa polja predstavljajo vodo.
            this.odkrit = false; //Na začetku so vsa polja neodkrita.
            this.sosed = false;  //Na začetku nobeno polje ni sosed.
            this.potopljen = false; //Na začetku nobeno polje ni potopljeno.
        }

RAZRED Polje - METODE

Razred Polje bo vseboval naslednje metode:

  • jeOdkrit()
  • odkrijPolje()
  • jeVoda()
  • jeSosed()
  • nastaviSosed(bool jeSosed)
  • nastaviLadjo(Ladja ladja)
  • jePotopljen()
  • potopiPolje()

METODI jeOdkrit() in odkrijPolje() - RAZRED Polje

Metoda jeOdkrit()

Metoda vrne true, če je polje že odkrito oz. false, če še ni.

Koda:

        public bool jeOdkrit()
        {
            return this.odkrit;
        }

Metoda odkrijPolje()

Metoda nastavi spremenljivko Polja odkrit na true in vrne true, če smo s to potezo potopili celotno ladjo oz. false če je nismo.

  • Če smo s to potezo potopili ladjo, pokličemo metodo zadeniLadjo(), ki jo kličemo na objektu razreda Ladja, ki ga hranimo v razredni spremenljivki ladja. Vrednost te spremenljivke dobimo z metodo vrniLadjo().

    • Funkcijo zadeniLadjo() bomo deklarirali kasneje znotraj razreda Ladja.

Koda:

        public bool odkrijPolje()
        {
            this.odkrit = true;

            if (this.ladja != null)//Če je polje del ladje.
            {
                return this.vrniLadjo().zadeniLadjo();//Vrnemo true, če je ladja potopljena oz. false, če ni.
            }
            return false;//Če gre za vodo, vrnemo false.
        }

METODE jeVoda(), jeSosed() in nastaviSosed() - RAZRED Polje

Metoda jeVoda()

Metoda vrne true, če je polje voda, torej kadar je spremenljivka ladja enaka null. Metoda vrne false, če je polje del ladje (ladja ni null).

Koda:

        public bool jeVoda()
        }
            return ladja == null;
        }

Metoda jeSosed()

Metoda vrne true, če je polje sosed kakšne ladje. Torej vrne kar vrednost razredne spremenljivke sosed.

  • Lastnost ali je sosed bomo potrebovali takrat, ko bomo naključno postavljali ladje. Ne smemo namreč dovoliti, da se dve ladji držita skupaj ali pa se križata.
  • Če je polje del ladje, ima spremenljivko sosed true.

Koda:

        public bool jeSosed()
        {
            return sosed;
        }

Metoda nastaviSosed(bool jeSosed)

Metoda spremenljivko sosed nastavi na podano vrednost jeSosed.

  • Metoda sprejme spremenljivko tipa bool (true ali false).

Koda:

        public void nastaviSosed(bool jeSosed)
        {
            this.sosed = jeSosed;
        }

METODI vrniLadjo() in nastaviLadjo() - RAZRED Polje

Metoda vrniLadjo()

Metoda vrne vrednost, ki jo ima razredna spremenljivka ladja.

  • Če vrne null, polje predstavlja vodo.
  • Če je polje del ladje, vrne kazalec na objekt tipa Ladja.

Koda:

        public Ladja vrniLadjo()
        {
            return ladja;
        }


Metoda nastaviLadjo(Ladja ladja)

Metoda polju priredi podano ladjo.

  • Metoda sprejme objekt razreda Ladja.

Koda:

        public void nastaviLadjo(Ladja ladja)
        {
            this.ladja = ladja;
        }

METODI jePotopljen() in potopiPolje() - RAZRED Polje

Metoda jePotopljen()

Metoda vrne true, če je polje že potopljeno, kar pomeni, da je polje del že v celoti potopljene ladje. Če spremenljivka vrne false pomeni, da je polje voda ali pa del še ne potopljene ladje.

Koda:

        public bool jePotopljen()
        {
            return potopljen;
        }


Metoda potopiPolje()

Metoda razredni spremenljivki potopljen nastavi na true.

Koda:

        public void potopiPolje()
        {
            this.potopljen = true;
        }

RAZRED Ladja

Spomnimo se, kaj potrebujemo vedeti o ladji:

  • Poznati moramo vsa polja, ki so del ladje.

    • tabela Polj - dolžina nujno med vključno 1 in vključno 5.
    • Dolžina tabele nam pove, za katero ladjo gre (ladje ločimo glede na število polj, ki jih zasedejo).
  • Število zadetih polj

    • Iz števila zadetih polj lahko sklepamo, ali je ladja potopljena.

Konstruktor:

Konstruktor sprejme tabelo Polj.

  • Če tabela nima ustrezne dolžine, sprožimo napako.
  • Predpostavimo, da je podan seznam polj v redu (torej, da se polja držijo skupaj in, da skupaj tvorijo bodisi navpično, bodisi vodoravno postavljeno ladjo).

Število zadetih polj je na začetku nič.

Znotraj konstruktorja se sprehodimo čez tabelo polj in vsakemu polju priredimo ladjo.

Koda:

        public Ladja(Polje[] polja)
        {
            if (polja.Length > 5 || polja.Length < 1)
            {
                //Sprožimo napako:
                throw new Exception("Napaka! Ladja ima lahko od vključno 1 do vključno 5 polj!");
            }
            this.polja = polja;
            this.stZadetihPolj = 0;//Na začetku je število potopljenih polj ladje enako 0.

            foreach (Polje p in polja)
            { //Vsem poljem, ki pripadajo h ladji, spremenimo lastnost ladja.
                p.nastaviLadjo(this); //Priredimo polju ladjo.
            }
        }

RAZRED Ladja - METODI

Razred Ladja bo vseboval naslednji metodi:

  • zadeniLadjo()
  • tipLadje()

METODA zadeniLadjo() - RAZRED Ladja

Metoda poveča število zadetih polj ladje za ena in hkrati preveri, če smo s to potezo ladjo potopili.

  • Metoda vrne true, če smo s to potezo v celoti potopili ladjo. Torej je število zadetih polj enako dolžini tabele Polj ladje (polja).

    • Če smo s to potezo ladjo potopili, vsem poljem ladje nastavimo lastnost potopljen na true.
  • Metoda vrne false, če smo s to potezo zgolj zadeli eno polje ladje, celotna ladja pa še ni potopljena.

Koda:

        public bool zadeniLadjo()
        {
            stZadetihPolj++; //Število zadetih polj povečamo za 1.
            if (stZadetihPolj == polja.Length) //Če smo potopili ladjo, vsa polja nastavimo na potopljena.
            {
                foreach (Polje p in polja) //Sprehodimo se čez vsa polja ladje.
                {
                    p.potopiPolje(); //Nastavimo lastnost potopljen na true.
                }
                return true; //Vrne ture, ker je ladja potopljena.

            }
            return false; //S to potezo nismo potopili ladje, vrnemo false.
        }

METODA tipLadje() - RAZRED Ladja

Metoda vrne tip ladje, kot niz.

  • Vrne tak niz, da ga lahko direktno uporabimo npr. v stavku: "Računalnik ti je potopil "+tip ladje;

Tip ladje določimo glede na število polj, ki jih ladja zasede:

  • Letalonosilka - 5 polj,
  • Bojna ladja - 4 polja,
  • Križarka - 3 polja,
  • Raketni čoln - 2 polji,
  • Podmornica - 1 polje.

Koda:

        public string tipLadje()
        {
            if (polja.Length == 5) //Ladja s petimi polji je LETALONOSILKA.
            {
                return "letalonosilko.";
            }
            else if (polja.Length == 4) //BOJNA LADJA
            {
                return "bojno ladjo.";
            }
            else if (polja.Length == 3) //KRIŽARKA
            {
                return "križarko.";
            }
            else if (polja.Length == 2) //RAKETNI ČOLN
            {
                return "raketni čoln.";
            }
            return "podmornico."; //PODMORNICA (zasede natanko eno polje).
        }

Nazaj v naš glavni program - DEKLARACIJA RAZREDNIH SPREMENLJIVK

Sedaj imamo pripravljena oba potrebovana razreda.

Kodo v ozadju programa bomo pisali v zavihku "frmPotapljanjeLadjic.cs".

  • Zavihek odpremo tako, da v podoknu "Solution Explorer" z desnim klikom kliknemo na "frmPotapljanjeLadjic.cs" in izberemo "View Code" ali pa enostavno dvakrat kliknemo na formo v zavihku [Design].

Vse spremenljivke na nivoju forme bodo privatne (private).

  • Z izrazom na nivoju forme mislimo na to, da bodo spremenljivke vidne v vseh metodah znotraj forme.

Na nivoju forme bomo hranili naslednje spremenljivke:

  • Obe mreži oz. dvodimenzionalni tabeli Polj.

    • private Polje[,] mrezaIgralec;
    • private Polje[,] mrezaRacunalnik;
  • Število potopljenih ladij na posamezni mreži/plošči.

    • int stPotopljenihIgr;

      • Število potopljenih ladij na mreži mrezaIgralec oz. število ladij, ki jih je potopil računalnik.
    • int stPotopljenihRac;

      • Število potopljenih ladij na plošči mrezaRacunalnik oz. število ladij, ki jih je potopil igralec.
    • Oba podatka bomo prikazovali kot informacijo na formi s pomočjo oznak lblStIgr in lblStRac.
  • Enota velikosti kvadratka plošče pnlRacunalnik.

    • int enota;
  • Generator naključnih števil, ki ga bomo uporabili pri naključnem postavljanju ladij in pri potezah računalnika.

    • Random gen;
  • Seznam zadnjih zadetih polj ladje, ki še ni potopljena.

    • Ta seznam bomo kasneje potrebovali pri programiranju naslednje (pametne) poteze računalnika.
    • List<Point> zadnjePoteze;

      • Gre za seznam točk.

Opomba

Opomba

Trenutno pri nekaterih spremenljivkah ne vemo še zakaj in kje jih bomo uporabili. Stvari bodo postale jasne tekom gradnje programa.

DOGODEK/METODA Load - frmPotapljanjeLadjic_Load(object sender, EventArgs e)

KAJ SE ZGODI, KO SE FORMA NALOŽI OZ. ODPREMO PROGRAM?

  • Ko se forma nalaga, se sproži dogodek Load.
  • Opomba

Vrednost, ki se ne bo spreminjala čez celotni projekt, je enota velikosti kvadratka za ploščo pnlRacunalnik. Zato jo lahko izračunamo na začetku.

  • To je res zgolj zato, ker bo naša forma konstantne velikosti.

Poleg enote, lahko na začetku naredimo še generator naključnih števil.

  • gen=new Random()

Vse ostale spremenljivke in lastnosti se bodo spreminjale glede na potek igre oz. začetek igre.

Pripravimo si neko novo metodo, ki ne vrača ničesar(void), npr. metodo novaIgra(). Ta metoda bo poskrbela za pripravo vseh spremenljivk in kontrol ter njihovih lastnosti za začetek igre.


Koda metode/dogodka Load

Koda metode/dogodka Load

        private void frmPotapljanjeLadjic_Load(object sender, EventArgs e)
        {
            enota = pnlRacunalnik.Width / 10; //Enota platna pnlRacunalnik
            gen = new Random(); //Generator naključnih števil
            novaIgra(); //"Sprožimo" novo igro
        }

Opomba

V zavihku, kjer se nahaja koda, Load metodo dobimo tako, da v [Design]zavihku dvakrat kliknemo na formo. Ob dvojnem kliku se nam odpre zavihek "frmPotapljanjeLadjic.cs", fokusirana je želena metoda.

  • Na začetku je metoda "frmPotapljanjeLadjic_Load(object sender, EventArgs e)" prazna.

METODA novaIgra()

Metodo novaIgra() bomo poklicali vsakič, ko bomo začeli novo igro.

  • Klic metode ob nalaganju forme (dogodek Load).
  • Klic metode ob kliku na gumb btnNovaIgra oz. gumb z besedilom Nova igra.

POTEK:

Znotraj metode bomo najprej na novo naredili obe dvodimenzionalni tabeli Polj velikosti 10x10. Omenjeni tabeli bosta predstavljali mreži obeh igralcev.

  • mrezaIgralec = new Polje [10,10];
  • mrezaRacunalnik = new Polje [10,10];

Nato bomo mreži napolnili s samimi neodkritimi polji, ki bodo za enkrat predstavljali samo vodo.

  • Za to spišemo novo funkcijo napolniMrezo(Polje [,] mreza).

    • napolniMrezo(mrezaIgralec);
    • napolniMrezo(mrezaRacunalnik);

Za tem bomo naključno na vsako mrežo postavili vseh možnih 7 ladjic. Ladjice bomo postavljali posamično.

  • Spet spišemo novo metodo dodajLadjo(int steviloPolj, Polje[,] mreza).

Na začetku igre je število potopljenih ladij igralca enako nič, prav tako je število potopljenih ladij računalnika enako nič.

  • Spremenljivkama stPotopljenihIgr in stPotopljenihRac priredimo vrednost 0.

Ustrezno oznakama lblStIgr in lblStRac spremenimo prikazano besedilo oz. lastnost Text.

  • lblStIgr.Text = "Število ladij, ki jih je potopil računalnik: 0";
  • lblStRac.Text = "Število ladij, ki si jih že potopil: 0";

Oznaka lblStatus na začetku ne prikazuje nobenega besedila. Njena lastnost Text je prazen niz.

  • lblStatus.Text = "";

Nato naredimo še nov seznam točk zadnjePoteze. Na začetku le-ta nima nobenega elementa, saj računalnik nima odkrite na začetku nobene ladje.

Za konec samo še sprožimo dogodka Paint za obe platni. Omenjen dogodek poskrbi za ponoven izris platna.

  • Dogodek Paint sprožimo s klicem Invalidate() na platnu.

    • pnlIgralec.Invalidate();
    • pnlIgralec.Invalidate();

Koda metode novaIgra()

Koda metode novaIgra()

        private void novaIgra()
        {
            //Pripravimo si 2D tabeli za hranjenje odkritih polj;
            mrezaIgralec = new Polje[10, 10];
            mrezaRacunalnik = new Polje[10, 10];

            //Napolnemo mreže
            napolniMrezo(mrezaIgralec);
            napolniMrezo(mrezaRacunalnik);


            //Na začetku ni potopljene še nobene ladje.
            stPotopljenihIgr = 0;
            stPotopljenihRac = 0;

            //Naključno postavljene ladje - pnlRacunalnik:
            dodajLadjo(5, mrezaRacunalnik); //Postavimo letalonosilko
            dodajLadjo(4, mrezaRacunalnik); //Postavimo bojno ladjo
            dodajLadjo(3, mrezaRacunalnik); //Postavimo križarko
            //Postavimo dva raketna čolna:
            dodajLadjo(2, mrezaRacunalnik);
            dodajLadjo(2, mrezaRacunalnik);
            //Postavimo dve podmornici:
            dodajLadjo(1, mrezaRacunalnik);
            dodajLadjo(1, mrezaRacunalnik);

            //Naključno postavljene ladje - pnlIgralec:
            dodajLadjo(5, mrezaIgralec); //Postavimo letalonosilko
            dodajLadjo(4, mrezaIgralec); //Postavimo bojno ladjo
            dodajLadjo(3, mrezaIgralec); //Postavimo križarko
            //Postavimo dva raketna čolna:
            dodajLadjo(2, mrezaIgralec);
            dodajLadjo(2, mrezaIgralec);
            //Postavimo dve podmornici:
            dodajLadjo(1, mrezaIgralec);
            dodajLadjo(1, mrezaIgralec);

            //Prikaz informacij s pomočjo label:
            lblStIgr.Text = "Število ladij, ki jih je potopil računalnik: 0";
            lblStRac.Text = "Število ladij, ki si jih že potopil: 0";
            lblStatus.Text = "";

            //Nov seznam zadnjih potez zadete ladje;
            zadnjePoteze = new List<Point>();

            //Sprožimo ponovni izris platnov:
            pnlIgralec.Invalidate();
            pnlRacunalnik.Invalidate();
        }

METODA napolniMrezo(Polje[,] mreza) - frmPotapljanjeLadjic

Metoda podano mrežo oz. tabelo polj napolni s samimi neodkritimi polji, ki predstavljajo vodo.

Sprehodimo se torej čez vse elemente podane tabele in vsakemu polju priredimo nov objekt razreda Polje.

  • Pomagamo si z dvojno for zanko. Zunanja for zanka gre od i=0 do vključno i=9, notranja pa od j=0 do vključno j=9. Kot smo že povedali, vsakemu elementu priredimo novo Polje.

Koda metode napolniMrezo(Polje[,] mreza)

Koda napolniMrezo(Polje[,] mreza)

               private void napolniMrezo(Polje[,] mreza)
        {
           //Podano mrežo napolnemo z vodo.

            //Sprehodimo se čez vse elemente tabele mreza:
            for (int i = 0; i < 10; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    mreza[i, j] = new Polje(); //(i,j)-temu elementu priredimo nov objekt razreda Polje, ki predstavlja vodo.
                }
            }
        }

METODA dodajLadjo(int steviloPolj, Polje[,] mreza) - frmPotapljanjeLadjic

Metoda doda ladjo na mrežo. Pozicijo izbere naključno. Metoda sprejme dva parametra:

  • int steviloPolj

    • steviloPolj nam pove, kako velika bo ladja in posledično katero ladjo želimo dodati.
  • Polje[,] mreza

    • 2. parameter je tabela Polj na katero želimo dodati ladjo.

Metoda ne vrača ničesar, zato je void.

Znotraj metode si najprej pripravimo 4 spremenljivke

  • int x in int y nam bosta povedali začetno polje ladje.
  • int rotacija nam bo povedala ali je ladja postavljena navpično ali vodoravno na mreži.

    • rotacija == 1 --> vodoravno postavljena ladja.
    • rotacija == 0 --> navpično postavljena ladja.
  • bool ok = false. Spremenljivka ok bo držala informacijo, če je ladjo z danim začetnim poljem (x,y) in izbrano rotacijo možno postaviti. Vemo namreč, da imamo več pogojev:

    • Ladje se ne smejo držati skupaj.
    • Celotna ladja mora biti znotraj mreže.

Dokler je spremenljivka ok enaka false, iščemo novo pozicijo ladje. Pomagamo si z while zanko.

Znotraj while zanke spremenljivko ok nastavimo na true in poiščemo naključno rotacijo. Torej s pomočjo generatorja števila gen, poiščemo naključno število, bodisi 0 bodisi 1. Nato glede na rotacijo ločimo dva primera.

VODORAVNA POSTAVITEV:

Če je rotacija enaka 0, bomo poizkusili ladjo postaviti vodoravno.

Določimo neko naključno začetno pozicijo/polje (x,y) s pomočjo generatorja naključnih števil gen:

  • x je naključno celo število med 0 in 10-steviloPolj.

    • 10-steviloPolj zato, ker želimo, da je celotna ladja znotraj mreže.
    • x = gen.Next(0,10-steviloPolj);
  • y je naključno število med 0 in 10.

    • Pri y-u ni omejitve, ker je y koordinata enaka pri vseh poljih ladje.

Glede na velikost se sprehodimo čez polja, kamor želimo postaviti ladjo. Pomagamo si s for zanko, kjer i teče od x-a do vključno x+steviloPolj-1. Za vsako polje (i, y) preverimo, če je prosto oz. (i,y) polje ni sosed. Če je polje že sosed kateri ladji, ok nastavimo na false in gremo z ukazom break ven iz zanke, ker vemo, da ta postavitev ni v redu. Ker bo ok false, se bo zanka izvedla ponovno oz. tolikokrat, dokler ne bomo našli ustrezne pozicije.

V kolikor se for zanka izteče in je ok enak true vemo, da smo našli ustrezno pozicijo za ladjo. Takrat naredimo tabelo Polj dolžine vrednosti spremenljivke steviloPolj.

  • Polje [] polja = new Polje [SteviloPolj];

Spet se s pomočjo for zanke sprehodimo čez polja ladje v mreži in jih hkrati dodajamo v našo novo tabelo polja.

  • i spet teče od x do vključno (x+steviloPolj-1). Polje, ki se nahaja v mreži na(i,y)-tem elementu, dodamo v tabelo polja na (i-x)-ti element. Nato temu polju označimo sosede. Zato poskrbi metoda oznaciSosede(i,y,mreza).

Ko se for zanka izteče, naredimo novo ladjo s tabelo polja.

  • Ladja ladja = new Ladja (polja);.

NAVPIČNA POSTAVITEV:

Če je rotacija enaka 1, bomo poizkusili ladjo postaviti navpično.

Podobno kot prej določimo naključno začetno polje ladje (x,y). Tokrat pazimo, da je x enak za vsa polja in se y spreminja.

Princip je podoben kot pri ladji, ki je postavljena vodoravno, le da, kot smo omenili, se spreminja y in ne x.

Torej, sprehodimo se čez polja in preverimo, če so vsa polja prosta oz. nobeno polje še ni sosed.

Če so vsa polja prosta, naredimo novo ladjo in vsa polja ladja označimo za sosede.


Koda metode dodajLadjo(int steviloPolj, Polje[,] mreza)

Koda dodajLadjo(int steviloPolj, Polje[,] mreza)

         private void dodajLadjo(int steviloPolj, Polje[,] mreza)
        {
            //Začetne koordinate ladje;
            int x;
            int y;

            int rotacija; //Rotacija ladje (navpično=1; vodoravno=0);

            bool ok = false;
            while (!ok)//Poiščemo vredu pozicijo za ladjo
            //ladje se ne smejo dotikati!
            {
                ok = true;
                rotacija = gen.Next(0, 2); //Naključno generiramo rotacijo ladje;

                if (rotacija == 0) //VODORAVNO
                {
                    y = gen.Next(0, 10);//y koordinata je konstantna!
                    x = gen.Next(0, 10 - steviloPolj);//Najbolj leva x-koordinata ladje

                    //Preverimo, če je pozicija OK:
                    for (int i = x; i < x + steviloPolj; i++)
                    {
                        if (mreza[i, y].jeSosed()) //Če je polje že zasedeno, pozicija ni OK!
                        {
                            ok = false;
                            break;//Izhod iz for zanke!
                        }
                    }
                    if (ok)//Našli smo OK pozicijo!
                    {
                        Polje[] polja = new Polje[steviloPolj];//Ustvarimo tabelo polj.

                        for (int i = x; i < x + steviloPolj; i++)//Tabeli polja dodamo polja ladje.
                        {
                            polja[i - x] = mreza[i, y];//Dodamo Polje
                            oznaciSosede(i, y, mreza);//Označimo sosede
                        }
                        Ladja ladja = new Ladja(polja);//Naredimo novo Ladjo.
                    }

                }
                else //NAVPIČNO
                {
                    y = gen.Next(0, 10 - steviloPolj);//Najbolj zgornja y-koordinata ladje
                    x = gen.Next(0, 10); //x koordinata je konstantna!

                    //Preverimo, če je pozicija OK:
                    for (int i = y; i < y + steviloPolj; i++)
                    {
                        if (mreza[x, i].jeSosed())//Če je polje že zasedeno, pozicija ni OK!
                        {
                            ok = false; //Če je polje že zasedeno, pozicija ni OK!
                            break;//Izhod iz for zanke!
                        }
                    }
                    if (ok)//Našli smo OK pozicijo!
                    {
                        Polje[] polja = new Polje[steviloPolj];//Ustvarimo tabelo polj.
                        for (int i = y; i < y + steviloPolj; i++)//Tabeli polja dodamo polja ladje.
                        {
                            polja[i - y] = mreza[x, i];//Dodamo Polje
                            oznaciSosede(x, i, mreza);//Označimo sosede
                        }
                        Ladja ladja = new Ladja(polja);//Naredimo novo Ladjo.
                    }
                }

            }

        }

METODA oznaciSosede(int x, int y, Polje[,] mreza)

  • Metoda oznaciSosede sprejme 3 argumente. Prva dva (int x in int y) nam povesta pozicijo polja, katerega sosede želimo označiti. Tretji argument je mreža, na kateri se nahaja podano polje.
  • Metoda ne vrača ničesar (je void) le spremeni lastnost sosed vsem sosednjim podanega polja in polja samega.

Na spodnji sliki imamo označene sosede polja, ki ima vse sosede. Upoštevati moramo dejstvo, da polje na robu mreže nima vseh sosedov!

(SosednjaPolja.png)
Sosednja polja (polje ni na robu mreže)

Iz slike razberemo, da označimo polja od x-1 dox+1 po x-osi in od y-1 do y+1 po y-osi(če nismo na robu).


BOLJ PODROBNO:

Torej, potrebovali bomo dvojno for zanko. V zunanji for zanki bo i tekel po indeksih stolpcev tabele od vključno x-1 do vključno x+1. V notranji for zanki pa bo j tekel po indeksih vrstic od y-1 do y+1.

Znotraj obeh for zank preverimo, če je polje (i,j) res v tabeli/mreži, torej velja:

  • i<=0<10 in j<=0<10

Če je polje (i,j) znotraj mreže, temu polju s pomočjo objektne metode nastaviSosed(true) nastavimo lastnost sosed na true.


Koda metode oznaciSosede(int x, int y, Polje[,] mreza)

Koda oznaciSosede(int x, int y, Polje[,] mreza)

                private void oznaciSosede(int x, int y, Polje[,] mreza)
        {
            ///Metoda označi kot sosede vse sosednje elemente podanega polja (x,y) v podani mreži.
            for (int i = x - 1; i < x + 2; i++)
            { //Sprehodimo se čez sosede (x-os)
                for (int j = y - 1; j < y + 2; j++) // (y-os)
                //Samo polje bomo tudi označili kot soseda;
                {
                    if (i >= 0 && i < 10 && j >= 0 && j < 10) //Če smo znotraj mreže
                    {
                        mreza[i, j].nastaviSosed(true);//Razredno spremenljivko polja nastavimo na true;
                    }
                }
            }
        }

RISANJE

Sedaj se moramo lotiti izrisovanja mreže na platno.

  • Izrisovali bomo čisto enostavno, za vsako polje bomo ustrezno izrisali kvadratek.
  • Velikost stranice kvadratka bomo dobili tako, da bomo širino ali višino celotnega platna delili z 10, torej s številom kvadratkov.

    • Vemo, da je platno kvadratno, za to smo že poskrbeli na začetku.

Metoda void narisiPolja(Graphics g, Panel platno, Polje[,] mreza, bool rac)

Za izrisovanje bomo spisali svojo metodo, ki bo sprejela 4 argumente:

  • Graphics g
  • Panel platno --> platno, na katerega bomo risali.
  • Polje [,]mreza --> tabelo/mrežo polj
  • bool rac --> Spremenljivka nam bo povedala, ali rišemo mrežo, ki pripada računalniku (tukaj ne izrisujemo neodkritih ladij). V kolikor izrisujemo mrežo, ki pripada igralcu (na njej so ladje, ki jih potaplja računalnik), izrisujemo tudi še nepotopljene ladje.

Razločili bomo pet vrst Polj in le-te bomo ločili glede na barvo:

(Barve.png)

Znotraj metode najprej izračunamo enoto kvadratka za podano platno.

  • Širino platna dobimo tako, da uporabimo kar lastnost platna Width. Lahko bi uporabili to višino platna Height.

    • platno.Width
    • Enota oz. velikost stranice enega kvadratka je tako int enotaRI = platno.Width/10;.

Kot smo omenili, bomo izrisovali na platno kvadratek po kvadratek. Z dvojno for zanko se tako sprehodimo čez vse elemente mreže.

Znotraj notranje for zanke najprej naredimo kvadrat z ustrezno pozicijo in velikostjo. Za to poskrbi funkcija Rectangle, ki jo kličemo na podanem objektu g. Omenjena funkcija sprejme štiri argumente.

  • Prva dva argumenta povesta pozicijo (x, y).

    • V našem primeru bo torej pozicija (i*enotaRI, j*enotaRI).
  • 3. in 4. argument pa povesta širino in višino pravokotnika. V našem primeru gre za kvadrat, zato sta širina in višina enaki.

    • Za širino in višino ne vzamemo celotne enote (odštejemo 1), ker želimo, da je ob izrisu vidna mreža.

Naredimo torej nov kvadrat:

  • Rectangle kvadrat = new Rectangle(i * enotaRI, j * enotaRI, enotaRI-1, enotaRI -1)

Kvadrat bomo narisali z naslednjim klicem:

  • g.FillRectangle(copic, kvadrat)

    • copic predstavlja čopič - objekt razreda Brush, ki ga pred klicem zgornje funkcije seveda deklariramo in ustvarimo. Čopiču podamo želeno barvo.
    • Kvadratke bomo ločili glede na barvo:

      • ZADETA VODA:

        • Če je polje voda in polje je odkrit, naj bo čopič modre barve (Color.RoyalBlue)
        • Brush copicVZ = new SolidBrush(Color.RoyalBlue);
      • POTOPLJENA LADJA

        • Če je polje del potopljene ladje, funkcija jePotopljen(), ki jo kličemo nad poljem mreza[i,j], vrne true. Čopič je v tem primeru temno rdeče barve (Color.Maroon).
      • ZADETA LADJA

        • Če polje ni voda in je odkrito, naj bo kvadratek oz. čopič (živo) rdeče barve (Color.Red).
      • NEZADETA LADJA

        • Če polje ni voda in polje še ni odkrito ter rišemo igralčevo ploščo (rac=false), potem je čopič svetlo rjave barve (Color.Sienna).
      • NEODKRITO POLJE

        • V vseh ostalih primerih gre za neodkrito polje in je kvadratek/čopič sive barve (Color.Gray).

Vse naštete primere seveda ločimo z if/else if/…/else stavki. Ko se le-ti izvedejo, imamo pripravljen čopič in samo še ustrezno izrišemo kvadratek:

  • g.FillRectangle(copic, kvadrat)


Koda metode narisiPolja(Graphics g, Panel platno, Polje[,] mreza, bool rac)

Koda narisiPolja(Graphics g, Panel platno, Polje[,] mreza, bool rac)

        private void narisiPolja(Graphics g, Panel platno, Polje[,] mreza, bool rac)
        {
            //Metoda na platno ustrezno izriše celotno mrežo.

            int enotaRI = platno.Width / 10; //Enota glede na izbrano ploščo, ki jo izrisujemo.

            //Sprehodimo se čez mrežo (rišemo element po element)
            for (int i = 0; i < 10; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    Rectangle kvadrat = new Rectangle(i * enotaRI, j * enotaRI, enotaRI-1, enotaRI -1); //Ustvarimo nov kvadrat.
                    //Prva dva parametra sta začetna pozicija, druge dva pa širina in višina.
                    //enotaRI-1 zato, da se ne bodo držali čisto skupaj in bo vidna mreža.

                    Brush copic; //Deklariramo nov čopič

                    if (mreza[i, j].jeVoda() && mreza[i, j].jeOdkrit()) //ZADETA VODA!
                    {
                        copic = new SolidBrush(Color.RoyalBlue); //Naredimo nov čopič ustrezne barve.
                    }
                    else if (mreza[i, j].jePotopljen())//POTOPLJENA LADJA!
                    {
                        copic = new SolidBrush(Color.Maroon);
                    }
                    else if (!mreza[i, j].jeVoda() && mreza[i, j].jeOdkrit()) //ZADETA LADJA!
                    {
                        copic = new SolidBrush(Color.Red);
                    }
                    else if (!mreza[i, j].jeVoda() &&!rac) //NEZADETA LADJA (igralčeva plošča)
                    {
                        copic = new SolidBrush(Color.Sienna);
                    }
                    else //NEODKRITO POLJE
                    {
                        copic= new SolidBrush(Color.Gray);
                    }
                    g.FillRectangle(copic, kvadrat);//NARIŠEMO ZAPOLNJEN KVADRAT USTREZNE BARVE.
                }
            }
        }

RISANJE - DOGODKA Paint

Na prejšnji prosojnici smo si pripravili splošno metodo narisiPolja(), ki nam na podano platno izriše trenutno mrežo.

Omenjeno funkcijo kličemo takrat, ko se sproži ponovni izris platna. Za to poskrbi dogodek Paint, ki je vezan na platno.

Dogodek Paint vsebuje dva parametra:

  • object sender (za nas nepomemben)
  • PaintEventArgs e --> S pomočjo e-ja bomo dobili objekt Graphics:

    • e.Graphics

      • Podamo kot enega od parametrov pri klicu funkcije narisiPolja().


pnlRacunalnik_Paint(object sender, PaintEventArgs e)

Znotraj te metode pokličemo metodo narisiPolja(), kateri podamo:

  • e.Graphics
  • pnlRacunalnik <-- Rišemo na računalnikovo platno.
  • mrezaRacunalnik <-- Izrisujemo stanje računalnikovega platna.
  • true <-- Nočemo, da se izrišejo neodkrite ladje.

Koda dogodka pnlRacunalnik_Paint(object sender, PaintEventArgs e)


pnlRacunalnik_Paint(object sender, PaintEventArgs e)

Znotraj te metode pokličemo metodo narisiPolja(), kateri podamo:

  • e.Graphics
  • pnlIgralec <-- Rišemo na računalnikovo platno.
  • mrezaIgralec <-- Izrisujemo stanje računalnikovega platna .
  • false <-- Izrisujemo tudi neodkrite ladje. Ker želimo, da igralec ve, kje ima postavljene ladje, ki jih cilja računalnik.

Koda dogodka pnlIgralec_Paint(object sender, PaintEventArgs e)

Koda dogodka pnlRacunalnik_Paint(object sender, PaintEventArgs e)

        private void pnlRacunalnik_Paint(object sender, PaintEventArgs e)
        {
            narisiPolja(e.Graphics, pnlRacunalnik, mrezaRacunalnik, true);
        }

Koda pnlIgralec_Paint(object sender, PaintEventArgs e)

        private void pnlIgralec_Paint(object sender, PaintEventArgs e)
        {
            narisiPolja(e.Graphics, pnlIgralec, mrezaIgralec, false);
        }

METODA konecIgre()

Metoda konecIgre() vrne true, če je konec igre oz. false, če ni.

KDAJ JE KONEC IGRE?

Igre je konec, ko eden od igralcev potopi vseh 7 ladij nasprotnika. Torej kadar ima ena od spremenljivk stPotopljenihIgr ali stPotopljenihRac vrednost 7.

V kolikor je omenjen pogoj uresničen:

  • Naredimo nov objekt z imenom result razreda DialogResult.
  • Uporabniku izpišemo sporočilo glede na to, kdo je zmagal.

    • Če je zmagal igralec, bomo v sporočilu izpisali: "ČESTITAM, ZMAGAL SI! Nova igra?".
    • Če je zmagal računalnik, bomo v sporočilu izpisali: "IZGUBIL SI! Nova igra?".
  • Sporočilo prikažemo na zaslon s pomočjo naslednjega ukaza:

    • result = MessageBox.Show(sporocilo, "Konec igre!", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    • Ko se izvede zgornji ukaz, se uporabniku prikaže naslednje okno. Spodnja slika prikazuje primer, ko zmaga igralec:

      (sporociloZmaga.png)
  • Igralec se odloči, ali želi ponovno igro oz. ali želi zaključiti igro.

    • Rezultat njegove odločitve se shrani v spremenljivko result.

      • Če igralec klikne na gumb No, je spremenljivka result enaka DialogResult.No. V tem primeru zapremo formo. To naredimo z ukazom this.Close().
      • V nasprotnem primeru, torej če je bil kliknjen gumb Yes, pa metoda vrne true.

V kolikor ni konec igre, torej prvi pogoj, da je eden od igralcev potopil vseh 7 ladij, ni resničen, pa metoda vrne false.


Koda metode konecIgre()

Koda konecIgre()

     private bool konecIgre()
        {
            ///Metoda konecIgre() vrne true, če je konec igre oz. false, če ni.
            ///V primeru, da je konec igre, uporabnika povpraša po novi igri.
            ///V kolikor uporabnik zavrne novo igro, se program zapre.


            if (stPotopljenihIgr == 7 || stPotopljenihRac == 7) // Preverimo, če je že konec igre
            {
                string sporocilo; //Sporočilo ob koncu igre.

                DialogResult result = new DialogResult();

                if (stPotopljenihRac == 7) //ZMAGA IGRALCA
                {
                    sporocilo = "ČESTITAM, ZMAGAL SI! \nNova igra?";
                }
                else //ZMAGA RAČUNALNIKA
                {
                    sporocilo = "IZGUBIL SI! \nNova igra?";
                }

                // Sporočilo prikažemo na zaslon. Povprašamo po novi igri?
                result = MessageBox.Show(sporocilo, "Konec igre!",
                                   MessageBoxButtons.YesNo,
                                   MessageBoxIcon.Asterisk);

                if (result == DialogResult.No) //Igralec zavrne novo igro --> KONČAMO IGRANJE
                {
                    this.Close(); //Zapremo formo
                }
                else //NOVA IGRA
                {
                    return true;//Igralec želi novo igro, torej vrnemo true.
                }
            }
            return false; //Ni še konec igre!
        }

POTEZA IGRALCA in dogodek pnlRacunalnik_Click

Igralec bo naredil svojo potezo tako, da bo kliknil na izbrano polje na računalnikovi mreži/platnu.

Glede na to, kam bo kliknil, se bo polje odkrilo. Če je polje že odkrito, se ne zgodi nič.

Ko uporabnik klikne nekam na platno, se sproži dogodek pnlRacunalnik_MouseClick.

  • Omenjena metoda ima zelo pomemben parameter MouseEventArgs e, s pomočjo katerega bomo določili, na katero polje je igralec kliknil.
  • Z e.Location.X dobimo koordinato x točke na platnu, na katero je igralec kliknil. Če omenjeno koordinato x delimo z enoto kvadratka, dobimo stolpec v tabeli.
  • Če podobno naredimo z y, dobimo še vrstico tabele.
  • int x = e.Location.X / enota;
  • int y = e.Location.Y / enota;

    • Pri obeh primerih gre za celoštevilsko deljenje.
    • (x,y) je polje, na katerega je kliknil igralec.

Če je polje znotraj tabele, potem odkrijemo polje oz. (x,y)-ti element tabele mrezaRacunalnik. To naredimo tako, da pokličemo metodo mrezaRacunalnik[x, y].odkrijPolje(). Njen rezultat shranimo v neko bool spremenljivko, npr. potopljena. Če je potopljena true, to pomeni, da smo s to potezo potopili ladjo.

Če smo potopili ladjo:

  • stPotopljenihRac povečamo za 1.
  • Spremenimo še lastnost Text oznake lblStRac, izpišemo novo število potopljenih:

    • lblStRac.Text = "Število ladij, ki si jih že potopil: "+ stPotopljenihRac;
  • Na zaslon s pomočjo lblStatus sporočimo, da smo potopili določeno ladjo (tip ladje dobimo s pomočjo objektne metode tipLadje() razreda Ladja).

    • Besedilo oznake lblStatus prikažemo z zeleno barvo. To naredimo tako, da spremenimo lastnost ForeColor oznake lblStatus na zeleno.
    • Ustrezno moramo seveda spremeniti tudi lblStatus.Text.

      • lblStatus.Text = "Potopil si računalnikovo " + mrezaRacunalnik[x, y].vrniLadjo().tipLadje();

Za tem preverimo, če je slučajno konec igre. To naredimo tako, da pokličemo funkcijo konecIgre().

  • Če je konec igre, torej vrne funkcija true, začnemo novo igro (pokličemo funkcijo novaIgra()).
  • Če še ni konec igre, računalnik naredi naslednjo potezo.

    • Klic funkcije naslednjaPoteza().

Opombi


Koda dogodka pnlRacunalnik_MouseClick(object sender, MouseEventArgs e)

Opombi

Opomba 1 - Kako do dogodka MouseClick?:

V [Design]-zavihku se postavimo na platno pnlRacunalnik, nato pa v Properties pod Events(strela) poiščemo dogodek MouseClick in nanj dvakrat kliknemo.

Opomba 2:

Kasneje bomo program izboljšali tako, da bo s pomočjo miškinega kazalca znano, ali je polje že odkrito ali ne.

Koda pnlRacunalnik_MouseClick(object sender, MouseEventArgs e)

   private void pnlRacunalnik_MouseClick(object sender, MouseEventArgs e)
        {
            //POTEZA IGRALCA (posledično nato tudi POTEZA RAČUNALNIKA)

            bool potopljenaLadja; //True, če smo z našim klikom potopili dokončno ladjo
            //Pozicija v tabeli:
            int x = e.Location.X / enota;
            int y = e.Location.Y / enota;

            if (x < 10 && y < 10 && !mrezaRacunalnik[x, y].jeOdkrit()) //Klik na neodkrito polje --> odkrijemo polje.
            {
                lblStatus.Text = "";
                potopljenaLadja = mrezaRacunalnik[x, y].odkrijPolje(); //Odkrijemo polje
                pnlRacunalnik.Invalidate(); //Ponovno izrišemo mrežo na platno.

                if (potopljenaLadja)  //Preverimo, če smo slučajno s potezo potopili celotno ladjo.
                {
                    stPotopljenihRac++; //Število potopljenih ladij računalnika povečamo za 1.

                    //Poskrbimo za informativne izpise s pomočjo oznak:
                    lblStRac.Text = "Število ladij, ki si jih že potopil: " + stPotopljenihRac;
                    lblStatus.ForeColor = Color.Green;
                    lblStatus.Text = "Potopil si računalnikovo " + mrezaRacunalnik[x, y].vrniLadjo().tipLadje();
                }

                if (konecIgre())//Če igralec s trenutno potezo zmaga:
                {
                    novaIgra(); //Sprožimo novo igro.
                }
                else //S to potezo še ni konec igre:
                {
                    naslednjaPoteza();//Računalnik naredi naslednjo potezo.
                }
            }

        }

METODA naslednjaPoteza()

Metoda naslednjaPoteza() poskrbi za naslednjo potezo računalnika. Torej izbere in cilja naključno polje.

Lahko bi to metodo naredili zelo enostavno. Torej, generirali bi naključna polja toliko časa, dokler ne bi zgenerirali nekega polja, ki do sedaj še ni bilo odkrito.

Lahko pa malo bolj "zakompliciramo" zadevo in naredimo nekoliko bolj pameten računalnik.

  • Kaj mislimo z izrazom bolj pameten? Mislimo na to, da bo računalnik, v kolikor bo imel že "načeto" oz. odkrito ladjo, ciljal okoli polj, kjer imamo že gorečo (a še ne potopljeno) ladjo. Torej tam se bo zadržal toliko časa, dokler ne bo potopil celotne ladje.

Razmislek

Imamo 3 možnosti:

  • Trenutno nimamo odkrite (še ne v celoti potopljene) nobene ladje.

    • V tem primeru generiramo naključno koordinate (x,y) toliko časa, dokler polje s koordinatami (x,y) še ni odkrito.
  • Trenutno imamo odkrito eno ladjo (še ne v celoti potopljeno), a le eno polje te ladje

    • Računalnik ima na voljo torej štiri sosednja polja, kjer je možno, da se nahaja drugi del ladje (gor, dol, levo in desno).
  • Trenutno imamo odkrito eno ladjo (še ne v celoti potopljeno) in ta ladja ima odkrito več kot eno polje.

    • Ko imamo odkriti vsaj dve polji vemo, kako je orientirana ladja (vodoravno ali navpično).
    • Glede na to, ima računalnik na voljo le dve možni polji, kjer se lahko nahaja ladja.

METODA naslednjaPoteza() - PODROBNA RAZLAGA

Pripravimo si spremenljivke:

Na začetku metode si pripravimo nekaj spremenljivk:

  • int x=0 in int y = 0 --> Ti dve spremenljivki bosta predstavljali trenutno polje (x,y).

    • Na začetku sta obe spremenljivki 0 - zgolj zato, da bosta deklarirani.
  • int smer = 0 --> Ta spremenljivka bo povedala v katero smer, od že zadetega polja ladje, bomo ciljali.
  • int ind = 0 --> Spremenljivka bo hranila število odkritih polj trenutno delno odkrite ladje.

    • Če trenutno nimamo odkrite nobene ladje, bo to število enako 0.

      • Teoretično ne bi potrebovali te spremenljivke, ker bi se lahko sklicevali na lastnost Count pri seznamu zadnjePoteze.
      • Spomnimo se: Seznam točk zadnjePoteze imamo definiran na ravni forme. Tukaj notri se skrivajo točke (objekti razreda Point), ki hranijo koordinate polj, trenutno odkrite (še ne potopljene) ladje.
      • Če trenutno nimamo odkrite in hkrati nepotopljene nobene ladje, je dolžina tega seznama 0.
  • bool odkrit = true --> V kolikor bomo našli polje, ki še ni odkrito, bo ta spremenljivka postala false.
  • bool potopljena = false --> Spremenljivka, ki bo postala true samo v primeru, ko bo računalnik s to potezo dokončno potopil celotno ladjo.

Sedaj imamo definirane vse spremenljivke, ki ji potrebujemo znotraj te metode.

Kot smo že omenili, dokler bo spremenljivka odkrit enaka true, iščemo novo polje (x,y). Pomagamo si z while zanko. Znotraj le-te ločimo na grobo 2 primera:

Število elementov seznama zadnjePoteze je enako 0

V tem primeru generiramo naključna x in y med vključno 0 in vključno 9.

  • Naključno poiščemo neko polje znotraj mreže.

Število elementov seznama zadnjePoteze je različno od 0

V tem primeru najprej določimo smer.

Ločimo 2 primera:

  • Če je število elementov seznama 1, imamo odkrito samo eno polje in imamo na voljo 4 smeri.

    • (NaslednjaPotezaSmer.png)
    • Spremenljivki smer priredimo neko naključno vrednost med vključno 0 in vključno 3.
  • Če je število elementov seznama večje kot 1, pa priredimo smer glede na rotacijo ladje.

    • Če je ladja postavljena VODORAVNO (vse točke seznama imajo isto koordinato y), spremenljivki smer priredimo naključno vrednost, bodisi 2 bodisi 3.

    • Če je ladja postavljena NAVPIČNO (vse točke imajo isto koordinato x), spremenljivki smer priredimo naključno bodisi vrednost 0, bodisi 1.

Sedaj imamo določeno smer. Glede na smer pa sedaj določimo polje.

Spet imamo 4 možnosti (toliko kolikor imamo možnih smeri):

  • Če je smer = 0 - pogledamo gor:

    • Koordinata x ostane enaka kot že pri odkritem polju ladje.

      • Koordinato dobimo s pomočjo katerekoli točke v seznamu zadnjePoteze in njene lastnosti X.
    • Koordinato y pa določimo s pomočjo točke na ničtem mestu seznama in njene lastnosti Y, kateri odštejemo 1.

      • To deluje zato, ker imamo vedno urejen seznam točk zadnjePoteze.
      • Če razmislimo, potrebujemo najmanjšo možno koordinato y polje oz. točk, ki predstavljajo že odkrite dele ladje. Nato se premaknemo za "1 gor".
      • Če bo do tega trenutka odkrito samo eno polje ladje, bo tudi v redu.
  • Če je smer = 1 - pogledamo dol:

    • Koordinata x ostane enaka kot že pri odkritem polju ladje.

      • Koordinato dobimo s pomočjo zadnje točke in njene lastnosti X v seznamu zadnjePoteze.
    • Koordinato y pa določimo s pomočjo točke na zadnjem mestu seznama in njene lastnosti Y, kateri prištejemo 1.

      • Točka na zadnjem mestu v seznamu zadnjePoteze bo imela največji možni y od odkritih polj ladje.

        • Če imamo odkrito le eno polje, tudi deluje. Takrat je zadnja točka na ničtem mestu v seznamu.
  • Če je smer = 2 - pogledamo levo:

    • Koordinato x določimo s pomočjo točke na ničtem mestu seznama in njene lastnosti X, kateri odštejemo 1.

      • Točka na ničtem mestu v seznamu zadnjePoteze bo imela najmanjši možni x od že odkritih polj ladje.
    • Koordinata y ostane enaka kot že pri odkritem oz. odkritih poljih ladje.
  • Če je smer = 3 - pogledamo desno:

    • Koordinato x določimo s pomočjo točke na zadnjem mestu seznama in njene lastnosti X, kateri prištejemo 1.

      • Točka na zadnjem mestu v seznamu zadnjePoteze bo imela največji možni x od odkritih polj ladje.
    • Koordinata y ostane enaka kot že pri odkritem oz. odkritih poljih ladje.

Glede na smer smo sedaj določili neko polje (x,y). Če omenjeno polje leži znotraj mreže, preverimo s pomočjo polja (x,y) v mreži mrezaIgralec na (x,y)-tem nad katerim kličemo metodo jeOdkrit(). Kar omenjena metoda vrne, si shranimo v spremenljivko odkrit.

V koliko ima spremenljivka odkrit vrednost false, smo našli polje, ki še ni odkrito in ga odkrijemo s pomočjo metode odkrijPolje(). Kar slednja metoda vrne, si zapolnemo v spremenljivko potopljena.

  • Ker smo odkrili novo polje, se mora sprememba pokazati tudi na našem platnu pnlIgralec. S pomočjo metode Invalidate(), ki jo kličemo nad omenjenim platnom, sprožimo ponoven izris platna.


Naša metoda se glede na to, ali smo s to potezo potopili ladjo ali ne, razdeli na dva dela:

  • Če smo s to potezo potopili celotno ladjo, torej če ima spremenljivka potopljena vrednost true, izvedemo naslednje korake:

    • seznam zadnjePoteze s pomočjo metode Clear() izpraznimo.

      • To naredimo zato, ker sedaj nimamo odkrite nobene ladje, ki še ni v celoti potopljena.
    • Z rdečo barvo izpišemo na zaslon sporočilo, da je računalnik potopil določeno ladjo.

      • Sporočilo prikažemo s pomočjo oznake lblStatus, kateri spremenimo lastnost Text - priredimo ji ustrezen niz.
    • Spremenljivki stPotopljenihIgr prištejemo 1.
    • Spremenimo lastnost Text oznaki lblStIgr, saj se je spremenilo število ladij, ki jih je potopil računalnik.
    • Preverimo, če je konec igre - klic metode konecIgre().

      • V kolikor je konec igre, pokličemo funkcijo novaIgra().
  • Če s to potezo še nismo potopili celotne ladje, seznamu zadnjePoteze dodamo točko s koordinatami (x,y) in nato seznam uredimo.

    • Opomba
    • Ker seznam zadnjePoteze spreminjamo in urejamo samo znotraj te metode, bo omenjen seznam vedno urejen.

Koda metode naslednjaPoteza()

Vodoravno postavljena ladja

(naslednjaPotezaVodoravno.png)

Navpično postavljena ladja

(naslednjaPotezaNavpicno.png)

Sortiranje seznama zadnjePoteze

SeznamzadnjePotezeželimo sortirati najprej po koordinatix, nato pa še po koordinatiy.

Najbolj naravno se nam zdi, da pokličemo metodo Sort(). Na žalost tukaj naletimo na problem, da metoda ne zna primerjati dveh točk.

Problem rešimo z naslednjim klicem:

  •        zadnjePoteze = (zadnjePoteze.OrderBy(p => p.X).ThenBy(p => p.Y)).ToList();
    • Z metodo OrderBy(p=>p.X) povemo, da želimo seznam najprej uredi po lastnost X. Z metodoThenBy(p=>p.Y)povemo, naj za tem uredi še po lastnosti točkeY. Z metodo toList() pa vse pretvorimo nazaj v seznam in shranimo nazaj v seznam zadnjePoteze.

Koda naslednjaPoteza()

        private void naslednjaPoteza()
        {
            //Metoda, ki naredi naslednjo potezo računalnika.

            //Pripravimo si spremenljivke.

            //Naključno polje (x,y)
            int x = 0;
            int y = 0;
            bool odkrit = true; //Ali je naključno polje že zasedeno oz. odkrito.
            bool potopljena = false; //Ali smo s to potezo potopili celotno ladjo.

            //Spremenljivki bomo potrebovali, v kolikor ima igralec načeto ladjo:
            int smer = 0; //V katero smer od zadetega polja ciljamo.
            int ind = 0; //Indeks zadnjega elementa v seznamu zadnjePoteze.


            while (odkrit) //Poiščemo ustrezno neodkrito polje
            {
                if (zadnjePoteze.Count == 0) //Če trenutno nimamo "načete" nobene ladje
                {
                    //Poiščemo neko naključno polje na mreži
                    x = gen.Next(0, 10);
                    y = gen.Next(0, 10);
                }
                else if (zadnjePoteze.Count > 0) //Če imamo "načeto" eno ladjo (ni še potopljena);
                {
                    ind = zadnjePoteze.Count - 1; //Indeks zadnjega elementa v seznamu zadnjePoteze.

                    if (zadnjePoteze.Count == 1)//Imamo odkrito samo eno polje ladje.
                    //možne so štiri poti (gor(0), dol(1), levo(2), desno(3));
                    {
                        smer = gen.Next(0, 4); //Naključno izberemo smer
                    }
                    else //Imamo odkrita vsaj dva polja.
                    //Možne imamo dve smeri (bodisi levo in desno, bodisi gor in dol);
                    {
                        if (zadnjePoteze[0].X == zadnjePoteze[1].X) //NAVPIČNO POSTAVLJENA LADJA
                        {
                            smer = gen.Next(0, 2);
                        }
                        else //VODORAVNO POSTAVLJENA LADJA
                        {
                            smer = gen.Next(2, 4);
                        }
                    }

                    //Glede na smer, izberemo polje:
                    if (smer == 0) //Pogledamo gor
                    {
                        x = zadnjePoteze[0].X; //x-i so za celo ladjo enaki.
                        y = zadnjePoteze[0].Y- 1;
                    }
                    else if (smer == 1) //pogledamo dol
                    {
                        x = zadnjePoteze[ind].X; //x-i so za celo ladjo enaki.
                        y = zadnjePoteze[ind].Y + 1;
                    }
                    else if (smer == 2) //levo
                    {
                        x = zadnjePoteze[0].X - 1;
                        y = zadnjePoteze[0].Y; //y-i so za celo ladjo enaki.
                    }
                    else if (smer == 3)//desno
                    {
                        x =zadnjePoteze[ind].X + 1;
                        y =zadnjePoteze[ind].Y;  //y-i so za celo ladjo enaki.
                    }

                }
                if (x >= 0 && y >= 0 && x < 10 && y < 10) //Preverimo, če je polje (x,y) znotraj mreže.
                {
                    odkrit = mrezaIgralec[x, y].jeOdkrit();//Preverimo, če je polje že odkrito.
                }

            }
            //NAŠLI SMO NEODKRITO POLJE, zato ga odkrijemo.
            potopljena = mrezaIgralec[x, y].odkrijPolje();
            pnlIgralec.Invalidate(); //Izrišemo na novo.

            if (potopljena)
            {
                //Odkrili smo delček ladje in hkrati potopili ladjo.
                zadnjePoteze.Clear(); //Nimamo več zadetih a ne potopljenih polj;

                //Izpišemo status in stevilo potepljenih ladij igralca povečamo za 1.
                lblStatus.ForeColor = Color.Red; //Spremenimo barvo napisa.
                Ladja ladja = mrezaIgralec[x, y].vrniLadjo();
                lblStatus.Text = "Računalnik ti je potopil " + ladja.tipLadje();
                stPotopljenihIgr++;

                lblStIgr.Text = "Število ladij, ki jih je potopil računalnik " + stPotopljenihIgr;

                if (konecIgre()) //Preverimo, če je konec igre.
                {
                    novaIgra(); //Sprožimo novo igro.
                }
            }
            else if (mrezaIgralec[x, y].vrniLadjo() != null)//Odkrili smo nov del ladje, a s potezo nismo potopili celotne ladje.
            {
                zadnjePoteze.Add(new Point(x, y));//Polje dodamo kot točko v seznam.

                //Uredimo po velikosti:
                zadnjePoteze = (zadnjePoteze.OrderBy(p => p.X).ThenBy(p => p.Y)).ToList();
            }

        }

DOGODKA pnlRacunalnik_MouseMove IN pnlRacunalnik_MouseLeave

PROBLEM:

Sedaj, ko se z miško postavimo na določeno polje, se miškin kazalec ne spremeni oz. je za vsa polja enak.

Program spremenimo tako, da se kazalec spreminja glede na to, kje je miška.

pnlRacunalnik_MouseMove

Pozicijo miške dobimo s pomočjo dogodka pnlRacunalnik_MouseMove. Omenjeni dogodek se sproži vsakič, ko se z miško premaknemo kjerkoli na platnu pnlRacunalnik.

Pozicijo miške dobimo podobno kot pri dogodku pnlRacunalnik_Click (s pomočjo parametra e).

  • x = e.Location.X / enota;
  • y = e.Location.Y / enota;

Če je polje (x,y) že odkrito, kazalec miške spremenimo z naslednjim ukazom:

  • this.Cursor = Cursors.No;

Če je polje še neodkrito, torej ga uporabnik s klikom lahko odkrije, pa kazalec miške spremenimo v roko z naslednjim ukazom:

  • this.Cursor = Cursors.Hand;

To pa še ni vse, ko uporabnik z miško zapusti naše platno pnlRacunalnik, se more naš kazalec miške spremeniti nazaj v puščico.


Koda dogodka pnlRacunalnik_MouseMove(object sender, MouseEventArgs e)


pnlRacunalnik_MouseLeave

Ko uporabnik z miško zapusti ploščo pnlRacunalnik, se sproži dogodek MouseLeave.

Ko se sproži omenjeni dogodek, kazalec miške nastavimo nazaj na puščico. To naredimo z ukazom:

  • this.Cursor = Cursors.Default

Koda dogodka pnlRacunalnik_MouseLeave(object sender, EventArgs e)

KAZALCI MIŠKE

(Kurzerji.png)

Koda pnlRacunalnik_MouseMove(object sender, MouseEventArgs e)

       private void pnlRacunalnik_MouseMove(object sender, MouseEventArgs e)
        {
            //Izračunamo pozicijo(polje) v tabeli
            int x = e.Location.X / enota;
            int y = e.Location.Y / enota;

            if (x < 10 && y < 10)
            { //Če je miška znotraj mreže:

                if (mrezaRacunalnik[x, y].jeOdkrit()) //Odkrito polje
                {
                    this.Cursor = Cursors.No; //Če je polje odkrito, ga igralec ne sme ponovno klikniti. Nastavimo ustrezen kurzor.
                }
                else //Neodkrito polje
                {
                    this.Cursor = Cursors.Hand;
                }
            }
        }

Koda pnlRacunalnik_MouseLeave(object sender, EventArgs e)

     private void pnlRacunalnik_MouseLeave(object sender, EventArgs e)
        {
            this.Cursor = Cursors.Default; //Ko z miško zapustimo Panel, nastavimo na navaden kurzor.
        }

ZAKLJUČEK

S tem smo "zgradili" preprosto aplikacijo za potezno igro Potapljanje ladjic.

  • Po želji ga lahko izboljšamo še tako, da programu dodamo ikono. To naredimo tako, da v [Design]-zavihku poiščemo lastnost forme Icon in naložimo želeno ikono.
  • Na povezavi (desno - Povezava do nalog in kviza) so pripravljene naloge za še dodatne izboljšave programa.

Tekom programiranja igrice smo:

  • PONOVILI:

    • Kako ustvariti projekt
    • Uporabo dogodkov
    • Risanje na platno
    • Razrede
    • ...
  • SE NAUČILI:

    • Kako se lotiti programiranja nekoliko "večjega" problema.
    • Kako si na konkretnem primeru pomagati z lastnimi razredi.
    • Spoznali smo dvodimenzionalne tabele v jeziku C#.
    • ...

PRENOS CELOTNEGA PROJEKTA

Prenos


PRENOS NAMESTITVENIH DATOTEK

Prenos


POVEZAVA DO NALOG IN KVIZA

Klik

PREDNOSTI IN SLABOSTI PROGRAMA

SLABOSTI

  • Računalnik ne upošteva, da se ladje ne smejo dotikati in je posledično igralcu lažje zmagati.
  • Mreži nimata označenih polj.
  • Ni različnih stopenj (npr. lahko, težko, zelo težko).

PREDNOSTI

  • Enostaven za uporabo
  • Hiter
  • Zabaven

SIMULACIJA IGRE

VIRI

0%
0%