Textový projekt 3

Projekt sme dostali do fázy, v ktorej sme schopní prenášať objekty z jedného brehu na druhý, avšak momentálne iba raz. Na zopakovanie celého doterajšieho procesu by sme potrebovali cyklus. Z troch druhov cyklov: for, whiledo-while môžeme vylučovacou metódou vyselektovať ten pravý:

  1. Nevieme na koľký raz sa individuálnemu hráčovi podarí hru vyriešiť, tým odpadá možnosť využitia cyklu for.
  2. Cyklus budeme musieť vykonať minimálne raz (aby hranie hry bolo vôbec možné), a to nám indikuje použitie cyklu do-while.

Jediné, čo teraz potrebujeme, je vymyslieť podmienku (okolnosti), vďaka ktorej sa cyklus ukončí. Prvé, čo by nám malo napadnúť, je, že sa hra ukončí, keď bude úspešne alebo neúspešne dokončená, avšak tak ďaleko nie sme, a okrem toho, každá hra by mala hráčovi dovoliť ukončenie kedykoľvek počas hrania. Najmä pre druhý dôvod by sme mali vymyslieť univerzálny spôsob ukončenia. Navrhujem „špeciálny príkaz“, na ktorý hra zareaguje, to jest, keď hráč namiesto objektu, ktorý má byť prevezený, zadá špeciálne kľúčové slovo, napríklad „koniec“. Ak máme ujasnený takýto cieľ, môžeme sa posunúť ďalej:

Z nasledujúcich riadkov:

vypíšStav();
príkaz = načítajReťazec("Čo má prievozník previezť?");
prevez(príkaz);
vypíšStav();

Stačí umiestniť do cyklu posledné tri. Načítame príkaz, posunieme ho do metódy prevez, vypíšeme stav a celé to zopakujeme, kým hráč nezadá koniec:

vypíšStav();
 
do {
    príkaz = načítajReťazec("Čo má prievozník previezť?");
    prevez(príkaz);
    vypíšStav();
} while (!príkaz.equalsIgnoreCase("koniec"));

Avšak je absurdné, aby sa hra pokúšala „previezť na druhú stranu rieky príkaz koniec“. Preto to vyriešime dodatočnou podmienkou:

vypíšStav();
 
do {
    príkaz = načítajReťazec("Čo má prievozník previezť?");
 
    if (!príkaz.equalsIgnoreCase("koniec"))
    {
        prevez(príkaz);
        vypíšStav();
    }
 
} while (!príkaz.equalsIgnoreCase("koniec"));

Teraz môžeme prevážať objekty z jedného brehu na druhý a späť, kým nás to neomrzí. Keď zadáme „koniec“, hra sa skončí a funguje aj prechod pltníka naprázdno (keď zadáme „nič“):

Na ľavom brehu je:
Na pravom brehu je: [prievozník] vlk koza kapusta
Čo má prievozník previezť? (reťazec): koza
 
Na ľavom brehu je:  [prievozník] koza
Na pravom brehu je: vlk kapusta
Čo má prievozník previezť? (reťazec): nič
 
Na ľavom brehu je:  koza
Na pravom brehu je: [prievozník] vlk kapusta
Čo má prievozník previezť? (reťazec): kapusta

Bez toho, aby mohol prejsť pltník na prázdno na druhý breh, by sa hra nedala dokončiť. Momentálne však máme v hre jednu nelogickosť. Ak sa hneď na prvýkrát pokúsime prejsť na druhú stranu naprázdno, hra nám to nedovolí:

Na ľavom brehu je:
Na pravom brehu je: [prievozník] vlk koza kapusta
Čo má prievozník previezť? (reťazec): nič
Na ľavom brehu nie je nič.

Je síce malá pravdepodobnosť, že to hráč bude skúšať, ani úspešné dokončenie hry tým nie je nijako ohrozené. V podstate by sme to nemuseli riešiť, ale ak nám to prekáža, dá sa to veľmi jednoducho napraviť. Stačí rozšíriť definície polí jednotlivých brehov o jeden prázdny prvok. Hráč o ňom nemusí vedieť (prázdne prvky sa na výstupe nevypisujú), nikomu to prekážať nebude, iba sa tým mierne „poopraví“ fungovanie hry:

private final String[] ľavýBreh  = {"nič", "nič", "nič", "nič"};
private final String[] pravýBreh = {"vlk", "koza", "kapusta", "nič"};

To bolo mierne „kozmetické“ odbočenie, vráťme sa späť k téme. Do úplného dokončenia hry nám chýbajú dve podstatné veci:

  1. Overenie prípadov, keď sa pasažieri požierajú navzájom.
  2. Overenie úspešného dokončenia hry – všetci pasažieri sú na druhom brehu rieky.

Optimálne bude, keď pre každý bod naprogramujeme samostatnú metódu, ktorú vzápätí využijeme v hlavnom vlákne programu – v do-while cykle. Metódu overujúcu požieranie definujme s návratovou hodnotou typu boolean, ktorá bude vypovedať o tom, či bol niektorý z pasažierov zožraný. Nazvime ju napríklad niektoNiekohoZožral. Rovnako i druhú metódu, tá bude slúžiť na overenie, či sa hra už úspešne skončila definujme s návratovou hodnotou typu boolean a nazvime ju napríklad hraSkončila.

Pre potreby obidvoch metód bude užitočné definovať pomocnú metódu zisťujúcu na ktorom brehu sa práve nachádza zadaný pasažier. Metóda bude fungovať rovnako ako metódy nájdiNaPravomBrehunájdiNaĽavomBrehu, ibaže namiesto celočíselnej hodnoty indexu bude vracať logickú hodnotu majúcu podobný význam ako premenná prievozníkJeNaPravomBrehu. Keďže pre potreby zisťovania úspešného dokončenia hry (metóda hraSkončila) je výhodnejšie vedieť, či je pasažier na ľavom brehu, definujeme túto metódu presne v tomto význame. Nazveme ju jeNaĽavomBrehu a jej definícia, ako bolo povedané, bude vyzerať podobne ako definícia metódy nájdiNaĽavomBrehu:

private boolean jeNaĽavomBrehu(String čo)
{
    for (int = 0; < ľavýBreh.length; ++i)
    {
        if (čo.equalsIgnoreCase(ľavýBreh[i])) return true;
    }
 
    return false;
}

Jej služby potom využijeme na začiatku metód niektoNiekohoZožralhraSkončila – postup bude taký, že najskôr na začiatku každej metódy pre každého pasažiera zistíme, na ktorom brehu sa nachádza, a na základe toho vytvoríme logiku toho, či niektorý z nich niekoho zožral, alebo či sa už hra skončila. Prvý problém (detekcia vzájomného požierania pasažierov) vyžaduje buď dobrú predstavivosť a logiku, alebo pero a papier na zhrnutie všetkých možností. Ak zosumarizujeme fakty – máme štyri objekty, ktoré môžu jestvovať v dvoch stavoch: vlk, koza, kapusta i prievozník sa môžu nachádzať na pravom alebo ľavom brehu rieky – vyplynie nám z toho, že celkovo môže nastať 16 rôznych situácií.

Ťažšie by sa nám pracovalo, keby išlo o úplne abstraktné objekty, no i tak si môžeme ukázať, ako sa pri riešení takýchto problémov dá postupovať s pomocou tabuliek a zoskupovania možností. Zhrňme najskôr do jednej tabuľky všetky možné situácie:

Vlk

Koza

Kapusta

Prievozník

Komentáre

pravý

pravý

pravý

pravý

[počiatočný stav]

pravý

pravý

pravý

ľavý

pasažieri sa strážia navzájom

pravý

pravý

ľavý

pravý

kozu stráži prievozník

pravý

pravý

ľavý

ľavý

vlk zožral kozu

pravý

ľavý

pravý

pravý

nikto nikoho neohrozuje

pravý

ľavý

pravý

ľavý

nikto nikoho neohrozuje

pravý

ľavý

ľavý

pravý

koza zožrala kapustu

pravý

ľavý

ľavý

ľavý

kapustu stráži prievozník

ľavý

pravý

pravý

pravý

kapustu stráži prievozník

ľavý

pravý

pravý

ľavý

koza zožrala kapustu

ľavý

pravý

ľavý

pravý

nikto nikoho neohrozuje

ľavý

pravý

ľavý

ľavý

nikto nikoho neohrozuje

ľavý

ľavý

pravý

pravý

vlk zožral kozu

ľavý

ľavý

pravý

ľavý

kozu stráži prievozník

ľavý

ľavý

ľavý

pravý

alternatívny koniec hry (tento stav zrejme nenastane)

ľavý

ľavý

ľavý

ľavý

[koniec hry]

Vidíme, že kritické sú štyri situácie – keď na hociktorom z brehov vlk požiera kozu alebo koza kapustu. Potrebujeme ich vyjadriť programovo – podmienkou, resp. logickým výrazom. To sme schopní urobiť najmenej dvoma rôznymi spôsobmi – buď zostavíme výraz, ktorý všetky štyri situácie „vymenuje“, alebo zostavíme „vzorec“, resp. logický výraz, ktorý bude poskytovať výsledok vyhovujúci (pravdivý) práve pre štyri kľúčové situácie. Pri tejto konfigurácii by boli z hľadiska počítača oba spôsoby približne rovnako výpočtovo náročné, ale z hľadiska človeka by sme sa náročnejšie dopracúvali k požadovanému výrazu pri zvolení druhého spôsobu, preto zvolíme prvý spôsob.

V úvode metód niektoNiekohoZožralhraSkončila v prvom rade získame logické hodnoty prítomnosti na niektorom z brehov pre vlka, kozu a kapustu (pre prievozníka ju poznáme od začiatku – je uchovaná v premennej prievozníkJeNaPravomBrehu). Naprogramovali sme len metódu jeNaĽavomBrehu, čo sme zdôvodnili výhodnosťou pri použití v metóde hraSkončila. Mohli by sme naprogramovať rovnakú metódu jeNaPravomBrehu, ale z pohľadu logických hodnôt je to zbytočné. Jej naprogramovanie a použitie by do programu neprinieslo žiadne nové informácie (na rozdiel od dvojice metód nájdiNaPravomBrehunájdiNaĽavomBrehu, z ktorých každá poskytuje jedinečný údaj). Takže iba pripomíname, že pre premenné určujúce pozíciu pasažierov platí opačná logická hodnota než pre prievozníka:

boolean vlkNaĽavom = jeNaĽavomBrehu("vlk");
boolean kozaNaĽavom = jeNaĽavomBrehu("koza");
boolean kapustaNaĽavom = jeNaĽavomBrehu("kapusta");

(Názvy premenných sme zjednodušili, aby sme si ušetrili písanie.)

Tieto tri premenné spolu s premennou prievozníkJeNaPravomBrehu využijeme na detekciu toho, či vlk zožral kozu:

if ((vlkNaĽavom && kozaNaĽavom && !kapustaNaĽavom && prievozníkJeNaPravomBrehu) ||
    (!vlkNaĽavom && !kozaNaĽavom && kapustaNaĽavom && !prievozníkJeNaPravomBrehu))
{
    // Áno, vlk zožral kozu…
}

a rovnako na detekciu toho, či koza zožrala kapustu:

if ((kozaNaĽavom && kapustaNaĽavom && !vlkNaĽavom && prievozníkJeNaPravomBrehu) ||
    (!kozaNaĽavom && !kapustaNaĽavom && vlkNaĽavom && !prievozníkJeNaPravomBrehu))
{
    // Áno, koza zožrala kapustu…
}

Opačná situácia – úspešné dokončenie hry – sa dá detegovať ľahko. Stačí overiť, či sú všetci traja pasažieri na ľavom brehu:

if (vlkNaĽavom && kozaNaĽavom && kapustaNaĽavom)
{
    // Hurá, dokončili sme hru!
}

Teraz tieto tri situácie vložíme do jednotlivých metód. Najskôr naprogramujeme metódu niektoNiekohoZožral, ktorá bude vracať hodnotu true v prípade, že nastal ten prípad, keď niekto niekoho zožral a false v opačnom prípade. Pri tejto metóde mierne porušíme dobré programátorské návyky a popri vrátení hodnoty true vypíšeme na výstup správu o tom, kto koho zožral. V skutočnosti by sa vypísanie takejto správy malo odohrať mimo tejto metódy (pretože v tomto prípade nejde o komplexnú metódu, akou bola metóda prevez; naopak metóda niektoNiekohoZožral je typická elementárna metóda a tá by nemala vykonávať odozvy priamo pre používateľa), mali by sme hľadať iný spôsob komunikácie tejto metódy so svojím okolím (napríklad formou celočíselnej hodnoty alebo vymenovacieho typu), ale pre urýchlenie a zjednodušenie túto zásadu porušíme. Metóda bude vyzerať takto:

private boolean niektoNiekohoZožral()
{
    boolean vlkNaĽavom = jeNaĽavomBrehu("vlk");
    boolean kozaNaĽavom = jeNaĽavomBrehu("koza");
    boolean kapustaNaĽavom = jeNaĽavomBrehu("kapusta");
 
    if ((vlkNaĽavom && kozaNaĽavom &&
        !kapustaNaĽavom && prievozníkJeNaPravomBrehu) ||
        (!vlkNaĽavom && !kozaNaĽavom &&
        kapustaNaĽavom && !prievozníkJeNaPravomBrehu))
    {
        System.out.println("Vlk zožral kozu!");
        return true;
    }
 
    if ((kozaNaĽavom && kapustaNaĽavom &&
        !vlkNaĽavom && prievozníkJeNaPravomBrehu) ||
        (!kozaNaĽavom && !kapustaNaĽavom &&
        vlkNaĽavom && !prievozníkJeNaPravomBrehu))
    {
        System.out.println("Koza zožrala kapustu!");
        return true;
    }
 
    return false;
}

Pri metóde hraSkončila neporušíme žiadnu zo zásad, naopak, ukážeme si jedno zjednodušenie. Predpokladajme, že hocikde v programe vznikne v závere booleovskej metódy táto situácia:

if (true == booleovskáPremenná)
{
    return true;
}
 
return false;

Tá úplne korešponduje s nasledujúcou situáciou:

if (booleovskáPremenná)
{
    return true;
}
else
{
    return false;
}

čiže (inak povedané) v prípade, že hodnota premennej typu boolean je true, vráti metóda true, inak vracia false (to znamená že jej návratová hodnota presne korešponduje s aktuálnou hodnotou premennej). Vtedy (ako možno tušíte) môžeme celú štruktúru if-else zjednodušiť na jediný príkaz:

return booleovskáPremenná;

Presne to použijeme v metóde hraSkončila s drobným rozdielom – v príkaze return sa nevyskytne rýdzo booleovská premenná, ale celý booleovský výraz (princíp je rovnaký – výsledok je vždy hodnota typu boolean):

private boolean hraSkončila()
{
    boolean vlkNaĽavom = jeNaĽavomBrehu("vlk");
    boolean kozaNaĽavom = jeNaĽavomBrehu("koza");
    boolean kapustaNaĽavom = jeNaĽavomBrehu("kapusta");
 
    return vlkNaĽavom && kozaNaĽavom && kapustaNaĽavom;
}

So zjednodušovaním by sme mohli ísť ešte ďalej, ale to nechám na pozornom čitateľovi… Použitie metód ponecháme na poslednú kapitolu tejto série…

Skompletizovaný príklad sa nachádza v prílohe.

Príloha 5 – tretia fáza textového projektu

Zobraziť | Prevziať