ScenarioStep
z rámce, do nějž se hra začleňuje. 6 * 7 * Tato třída poskytuje definice používající přímé zadávání textů. 8 * Slouží pouze k demonstraci rozdílu oproti třídě (správci scénářů) 9 * používající konstanty a nejsou u ní proto průběžně upravovány detaily tak, 10 * aby s její pomocí byla hra doopravdy testovatelná. 11 *
12 * Správce scénářů je jedináček, který má na starosti všechny scénáře 13 * týkající se s ním sdružené hry. 14 */ 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 44 z 240
45
Kapitola 3: Návrh správce scénářů konkrétní hry 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
public class ManagerWithLiterals extends AScenarioManager { //== CONSTANT CLASS ATTRIBUTES ================================================= /** Třída hry, jejíž scénáře jsou zde spravovány. * Dokud neexistuje třída hry, je v atributu prázdný odkaz. * Jakmile je třída hry definována, je třeba do atributu umístit * odkaz na class-soubor příslušné třídy hry. */ private final static Class extends IGame> CLASS = null; /** Jméno autora dané třídy. */ private static final String AUTHOR_NAME = "PECINOVSKÝ Rudolf"; /** Xname autora/autorky dané třídy. */ private static final String AUTHOR_ID = "RUPxxY"; /** Pomocné konstanty pro rozhovor s ledničkou. */ private static final int AGE = 20; private static final int THIS_YEAR; private static final int BORN_YEAR; static { Calendar cal = new GregorianCalendar(); THIS_YEAR = cal.get(Calendar.YEAR); BORN_YEAR = THIS_YEAR - AGE; } /*************************************************************************** * Počáteční krok hry, který je pro všechny scénáře shodný. *
* Konstruktor plnohodnotné instance třídy {@link ScenarioStep} * vyžaduje následující parametry: <pre> {@code TypeOfStep typeOfStep; //Typ daného kroku scénáře String command; //Příkaz realizující tento krok scénáře String message; //Zpráva vypsaná po zadání příkazu String area; //Prostor, v němž skončí hráč po zadání příkazu String[] neighbors; //Sousedé aktuálního prostoru (= východy) String[] objects; //Objekty vyskytující se v daném prostoru String[] bag; //Aktuální obsah batohu } =======================================================================
* Kroky scénáře musejí navíc vyhovovat následujícím požadavkům: *
42 * U správce scénářů se prověřuje, zda vyhovuje zadaným okrajovým podmínkám, 43 * tj. jestli: 44 *
73 * U hry se prověřuje, zda je možno ji zahrát přesně tak, 74 * jak je naplánováno ve scénářích. 75 * 76 * @param args Parametry příkazového řádku - nepoužívané. 77 */ 78 public static void main(String[] args) 79 { 80 //Otestuje, zda správce scénářů a jeho scénáře vyhovují požadavkům 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 51 z 240
52 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
Návrh semestrálního projektu a jeho rámce – Adventura MANAGER.autoTest(); // // // // // // // //
//Testování hry prováděné postupně podle obou povinných scénářů MANAGER.testGame(); //Testování hry dle scénáře se zadaným názvem MANAGER.testGameByScenario("???"); //Odehrání hry dle scénáře se zadaným názvem MANAGER.playGameByScenario("???"); }
System.exit(0);
}
Hlavní metoda Výpis 3.3 končí definicí hlavní metody. V ní jsou připraveny příkazy pro otestování různých částí vytvářeného projektu. Ve výpisu je většina z nich zakomentovaná. „Živý“ je pouze příkaz žádající vytvořeného správce scénářů, aby se otestoval. Spustím-li jej pro vzorového správce scénářů, získám výstup, který najdete ve výpisu 3.4. Na začátku výpisu (řádky 2 – 6) se dozvíte, jako jsou požadavky na „rozměry“ úspěšného scénáře a za nimi pak následuje oznámení čí správce je testován, úplný název testované třídy a podpis prvního kroku, v němž by měla hra oznámit svůj základní námět. Následuje a hrubý popis toho, na co testovací program přišel. Výpis 3.4: 1 2 3 4 5 6 7 8 9
Výpis autotestu správce scénářů
Verze frameworku: 14.03.5077 — 2014-11-26 Minimální požadované "rozměry" úspěšného scénáře: Minimální počet kroků = 10 Minimální počet prostorů hry = 6 Minimální počet navštívených prostorů = 4 Minimální počet vlastních, tj. nepovinných příkazů = 4
Testuji správce scénářů autora: RUP14P – PECINOVSKÝ Rudolf Instance třídy: cz.pecinovsky.adv_framework.test_util.default_game.ManagerWithLiterals 10 ########## START: St 2014-11-26 — 14:53:12 11 12 13 0. krok: «» 14 ----------------------------------------------------------------------------15 Očekávaný stav po provedení akce: 16 0. krok 17 Typ kroku: tsSTART 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 52 z 240
53
Kapitola 3: Návrh správce scénářů konkrétní hry 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
Příkaz: Prostor: Předsíň Východy: («Koupelna», «Ložnice», «Obývák») Objekty: («Botník», «Deštník») Batoh: () ----------------------------------------------------------------------------Zpráva: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Vítáme vás ve služebním bytě. Jistě máte hlad. Najděte v bytě ledničku - tam vás čeká svačina. Nacházíte se v místnosti: Předsíň Můžete se přesunout do místností: Ložnice, Obývák, Koupelna V místnosti se nachází: Botník, Deštník Máte v držení objekty: ----------------------------------------------------------------------------############################################################################# Autor: PECINOVSKÝ Rudolf Třída správce: class cz.pecinovsky.adv_framework.test_util.default_game.ManagerWithLiterals Scénář: _HAPPY_ ===== Start testu ===== 26.11.2014 - 14:53:12 0. tsSTART 1. tsMOVE - jdi koupelna 2. tsPICK_UP - vezmi brýle 3. tsPICK_UP - vezmi časopis 4. tsMOVE - jdi předsíň 5. tsMOVE - jdi obývák 6. tsMOVE - jdi kuchyň 7. tsNON_STANDARD - otevři lednička 8. tsBAG_FULL - vezmi papír 9. tsPUT_DOWN - polož časopis 10. tsPICK_UP - vezmi papír 11. tsNON_STANDARD - přečti papír 12. tsNON_STANDARD - nasaď brýle 13. tsNON_STANDARD - přečti papír 14. tsPICK_UP - vezmi časopis 15. tsNON_STANDARD - podlož lednička časopis 16. tsPUT_DOWN - polož papír 17. tsNON_STANDARD - podlož lednička časopis 18. tsNON_STANDARD - otevři lednička 19. tsUNMOVABLE - vezmi pivo 20. tsDIALOG - 20 21. tsDIALOG - 1994 22. tsNON_STANDARD - zavři lednička 23. tsEND - konec ===== Konec testu ===== Kroků testu: 24 - vyhovuje Vlastních příkazů: 5 - vyhovuje Zmíněno místností: 6 - vyhovuje
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 53 z 240
54
Návrh semestrálního projektu a jeho rámce – Adventura
69 Z toho navštíveno: 5 - vyhovuje 70 Zadané akce: 9 - [jdi, konec, nasaď, otevři, podlož, polož, přečti, vezmi, zavři] 71 Neprovedených typů akcí: 11 - [tsHELP, tsEMPTY, tsUNKNOWN, tsMOVE_WA, tsPICK_UP_WA, tsPUT_DOWN_WA, tsBAD_NEIGHBOR, tsBAD_OBJECT, tsNOT_IN_BAG, tsDEMO, tsNOT_SET] 72 Z toho povinných: 0 - [] 73 Navštívené prostory: [koupelna, kuchyň, lednička, obývák, předsíň] 74 Nenavštívené prostory: [ložnice] 75 Zmíněné objekty: [botník, brýle, deštník, houska, lednička, papír, pivo, rum, salám, televize, umyvadlo, víno, časopis] 76 ===== Test ukončen 77 Autor: PECINOVSKÝ Rudolf 78 Třída správce: class cz.pecinovsky.adv_framework.test_util.default_game.ManagerWithLiterals 79 Scénář: _HAPPY_ 80 ===== Scénář vyhověl 81 ############################################################################# 82 83 84 ############################################################################# 85 Autor: PECINOVSKÝ Rudolf 86 Třída správce: class cz.pecinovsky.adv_framework.test_util.default_game.ManagerWithLiterals 87 Scénář: _MISTAKE_ 88 ===== Start testu ===== 26.11.2014 - 14:53:12 89 -1. tsNOT_START - Start 90 0. tsSTART 91 1. tsUNKNOWN - maso 92 2. tsEMPTY 93 3. tsPICK_UP - vezmi deštník 94 4. tsMOVE - jdi koupelna 95 5. tsBAD_NEIGHBOR - jdi záchod 96 6. tsBAD_OBJECT - vezmi koupelna 97 7. tsUNMOVABLE - vezmi umyvadlo 98 8. tsNOT_IN_BAG - polož papír 99 9. tsPICK_UP - vezmi brýle 100 10. tsBAG_FULL - vezmi Časopis 101 11. tsHELP - ? 102 12. tsEND - konec 103 Nepokryté typy akcí: [tsMOVE_WA, tsPICK_UP_WA, tsPUT_DOWN_WA] 104 ===== Konec testu ===== 105 Kroků testu: 13 106 Vlastních příkazů: 5 107 Zmíněno místností: 4 108 Z toho navštíveno: 2 109 Zadané akce: 5 - [?, jdi, konec, polož, vezmi] 110 Neprovedených typů akcí: 8 - [tsPUT_DOWN, tsMOVE_WA, tsPICK_UP_WA, tsPUT_DOWN_WA, tsDIALOG, tsNON_STANDARD, tsDEMO, tsNOT_SET] 111 Z toho povinných: 3 - [tsPUT_DOWN_WA, tsPICK_UP_WA, tsMOVE_WA] 112 Navštívené prostory: [koupelna, předsíň] 113 Nenavštívené prostory: [ložnice, obývák] 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 54 z 240
55
Kapitola 3: Návrh správce scénářů konkrétní hry Zmíněné objekty: [botník, brýle, deštník, umyvadlo, časopis] ===== Test ukončen Autor: PECINOVSKÝ Rudolf Třída správce: class cz.pecinovsky.adv_framework.test_util.default_game.ManagerWithLiterals 118 Scénář: _MISTAKE_ 119 ===== Scénář NEVYHOVĚL 120 ############################################################################# 114 115 116 117
Jak se můžete ve výpisu přesvědčit, úspěšný scénář testu vyhověl, ale neúspěšný ne. Příčina tohoto nevyhovění je oznámena na řádku 111, kde se můžete dozvědět, že scénář neobsahuje kroky tří povinných typů – konkrétně testy toho, jak bude hra reagovat, když hráč zapomene zadat, kam se má přesunout, resp. co se má zvednout, resp. co se má položit.
3.4
Trochu chytřejší správce
Výše uvedená definice má jednu nevýhodu: kdykoliv se budeme chtít ve hře odkázat na některý z textů ze scénáře, budeme muset tento text zkopírovat. Pokud jej budeme chtít později změnit (najdeme v něm překlep nebo se rozhodneme pro lepší formulaci), budeme muset najít všechny jeho výskyty a tuto opravu do nich zanést. Testovací program navíc nebude spokojen s naším řešením ani tehdy, když někde v textu posílaných zpráv a názvů pojmenovaných objektů (příkazů, prostorů a objektů) uděláme nějaký, byť malý překlep. Někde přehlédneme nenápadnou mezeru a budeme se divit, co že je na našem programu špatně. Programátorsky výhodnější řešení je proto definovat (nejlépe v odděleném zdrojovém souboru) sadu textových konstant a nahradit ve správci scénářů textové literály těmito konstantami. Scénář tím sice trochu ztratí na své přehlednosti, ale na druhou stranu budeme moci v budoucnu měnit texty vždy jenom na jediném místě – v definici příslušné konstanty. Uvedené řešení má další výhodu: rozhodnete-li se v budoucnu program lokalizovat do jiného jazyka, bude stačit změnit pouze třídu definující jednotlivé konstanty a zbytek programu může zůstat jaký je.
3.5
Definice třídy Texts
Definici třídy Texts definující jednotlivé textové konstanty pro naši hru si můžete prohlédnout ve výpisu 3.5.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 55 z 240
56
Návrh semestrálního projektu a jeho rámce – Adventura
Výpis 3.5: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
Definice třídy Texts definující jednotlivé textové konstanty
/******************************************************************************* * Knihovní třída {@code Texts} slouží jako schránka na textové konstanty, * které se používají na různých místech programu. * Centralizací definic těchto textových řetězců lze nejsnadněji dosáhnout toho, * že texty, které mají být shodné na různých místech programu, * budou doopravdy shodné. */ class Texts { //== CONSTANT CLASS ATTRIBUTES ================================================= /** Jméno autora programu. */ static final String AUTHOR_NAME = "PECINOVSKÝ Rudolf"; /** Xname autora programu. */ static final String AUTHOR_ID = "PECR999"; /** Názvy používaných prostorů - místností. */ static final String PŘEDSÍŇ = "Předsíň", LOŽNICE = "Ložnice", OBÝVÁK = "Obývák", KOUPELNA= "Koupelna", KUCHYŇ = "Kuchyň"; /** Názvy používaných objektů. */ static final String BOTNÍK = "Botník", DEŠTNÍK = "Deštník", BRÝLE = "Brýle", UMYVADLO= "Umyvadlo", TELEVIZE= "Televize", ČASOPIS = "Časopis", LEDNIČKA= "Lednička", PAPÍR = "Papír", PIVO = "Pivo", RUM = "Rum", SALÁM = "Salám", HOUSKA = "Houska", VÍNO = "Víno", POSTEL = "Postel", ZRCADLO = "Zrcadlo", ŽUPAN = "Župan"; /** Názvy používaných příkazů. */ static final String pHELP = "?", pJDI = "Jdi",
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 56 z 240
57
Kapitola 3: Návrh správce scénářů konkrétní hry 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
pNASAĎ pOTEVŘI pPODLOŽ pPOLOŽ pPŘEČTI pVEZMI pZAVŘI pKONEC
= = = = = = = =
"Nasaď", "Otevři", "Podlož", "Polož", "Přečti", "Vezmi", "Zavři", "Konec";
/** Formát dodatku zprávy informujícího o aktuálním stavu hráče. */ static final String SOUSEDÉ = "Sousedé: ", OBJEKTY = "Objekty: ", BATOH = "Batoh: ", FORMÁT_INFORMACE = "\n\nNacházíte se v místnosti: %s" + "\n" + SOUSEDÉ + "[%s]" + "\n" + OBJEKTY + "[%s]" + "\n" + BATOH + "[%s]"; /** Texty zpráv vypisovaných v reakci na povinné příkazy. * Počáteční z (zpráva) slouží k odlišení od stavů. */ static final String zNENÍ_START = "\nPrvním příkazem není startovací příkaz." + "\nHru, která neběží, lze spustit pouze startovacím příkazem.\n", zPORADÍM = "\nChcete-li poradit, zadejte příkaz ?", zPRÁZDNÝ_PŘÍKAZ = "\nZadal(a) jste prázdný příkaz." + zPORADÍM, zNEZNÁMÝ_PŘÍKAZ = "\nTento příkaz neznám." + zPORADÍM, zANP
= "\nZadaná akce nebyla provedena",
zPŘESUN zCIL_NEZADAN zNENÍ_CIL
= "\nPřesunul(a) jste se do místnosti: ", = zANP + "\nNebyla zadána místnost, do níž se má přejít", = zANP + "\nDo zadané místnosti se odsud nedá přejít",
zZVEDNUTO zPOLOŽENO zOBJEKT_NEZADAN zTĚŽKÝ_OBJEKT zNENÍ_OBJEKT zNENÍ_V_BATOHU zBATOH_PLNÝ
= = = = = = =
zNÁPOVĚDA
= "\nPříkazy, které je možno v průběhu hry zadat:" + "\n============================================\n",
"\nVzal(a) jste objekt: ", "\nPoložil(a) jste objekt: ", zANP + "\nNebyl zadán objekt, s nímž mám manipulovat", zANP + "\nZadaný objekt nejde zvednout: ", zANP + "\nZadaný objekt v místnosti není: ", zANP + "\nObjekt není v batohu: ", zANP + "\nZadaný objekt nemůžete vzít, máte už obě ruce plné",
zUVÍTÁNÍ = "\nVítáme vás ve služebním bytě. Jistě máte hlad." + "\nNajděte v bytě ledničku - tam vás čeká svačina.",
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 57 z 240
58 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
Návrh semestrálního projektu a jeho rámce – Adventura zCELÉ_UVÍTÁNÍ
= zUVÍTÁNÍ + String.format(FORMÁT_INFORMACE, PŘEDSÍŇ, cm(LOŽNICE, OBÝVÁK, KOUPELNA), cm(BOTNÍK, DEŠTNÍK), cm()),
zKONEC
= "\nKonec hry. \nDěkujeme, že jste zkusil(a) naši hru.";
/** Texty zpráv vypisované v reakci na nepovinné příkazy. */ static final String zLEDNICE_NEJDE_OTEVŘÍT = "\nLednička nejde otevřít. Na ledničce leží nějaký popsaný papír.", zCHCE_PŘEČÍST = "\nRozhodl(a) jste se přečíst ", zNEMÁ_BRÝLE = "." + "\nText je psán příliš malým písmem, které je rozmazané." + "\nMusíte si nasadit brýle.", zNASADIL_BRÝLE = "\nNasadil(a) jste si brýle.", zNAPSÁNO_PAPÍR = "." + "\nNa papíru je napsáno:" + "\nLednička stojí nakřivo, a proto jde špatně otevírat." + "\nBudete-li mít problémy, něčím ji podložte.", zCHCE_PODLOŽIT = "\nRozhodl(a) jste se podložit objekt ", zOBJEKTEM = " objektem ", zNELZE_NADZVEDNOUT = "\nBohužel máte obě ruce plné a nemáte ledničku čím nadzvednout." , zLEDNIČKA_PODLOŽENA = "\nLednička je úspěšně podložena - nyní by již měla jít otevřít.", zOTEVŘEL_LEDNIČKU = "\nÚspěšně jste otevřel(a) ledničku.", zBERE_ALKOHOL = "\nPokoušíte si vzít z inteligentní ledničky ", zKOLIK_LET = "\nToto je inteligentní lednička, která neumožňuje " + "\npodávání alkoholických nápojů mladistvým." + "\nKolik je vám let?",
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 58 z 240
Kapitola 3: Návrh správce scénářů konkrétní hry 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
59
zNAROZEN = "\nV kterém roce jste se narodil(a)?", zODEBRAL = "\nVěřím vám a předávám vám požadovaný nápoj." + "\nOdebral(a) jste z ledničky: ", zNEZAPOMEŇ = "\nDobrou chuť. Nezapomeňte zavřít ledničku.", zZAVŘEL_LEDNIČKU = "\nÚspěšně jste zavřel(a) ledničku.";
//== VARIABLE CLASS ATTRIBUTES =================================================
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= //== OTHER NON-PRIVATE CLASS METHODS =========================================== /*************************************************************************** * Vrátí řetězec obsahující zadané názvy oddělené čárkami. * * @param názvy Názvy, které je třeba sloučit * @return Výsledný řetězec ze sloučených zadaných názvů */ static String cm(String... názvy) { String result = Arrays.stream(názvy) .collect(Collectors.joining(", ")); return result; }
//== PRIVATE AND AUXILIARY CLASS METHODS =======================================
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== //== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS ===========================================
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 59 z 240
60 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
Návrh semestrálního projektu a jeho rámce – Adventura /** Soukromý konstruktor zabraňující vytvoření instance.*/ private Texts() {}
//== //== //== //==
ABSTRACT METHODS ========================================================== INSTANCE GETTERS AND SETTERS ============================================== OTHER NON-PRIVATE INSTANCE METHODS ======================================== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= }
3.6
Definice správce scénářů využívající konstanty
Správce scénářů využívající konstanty se bude od správce používajícího textové literály lišit pouze v konkrétní podobě zadání textů v definicích kroků scénáře (viz definice prvních tří kroků úspěšného scénáře ve výpisu 3.6). Jinak budou oba stejné. Výpis 3.6:
Definice prvních tří kroků úspěšného scénáře ve třídě ManagerWithConstants
1 /*************************************************************************** 2 * Počáteční krok hry, který je pro všechny scénáře shodný. 3 */ 4 private static final ScenarioStep START_STEP = 5 new ScenarioStep(tsSTART, "", //Název startovního příkazu 6 zCELÉ_UVÍTÁNÍ, 7 8 PŘEDSÍŇ, 9 new String[] { LOŽNICE, OBÝVÁK, KOUPELNA }, 10 new String[] { BOTNÍK, DEŠTNÍK }, 11 new String[] {} 12 ); 13 14 15 /*************************************************************************** 16 * Kroky základního úspěšného scénáře 17 * popisující očekávatelný úspěšný průběh hry. 18 * Z těchto kroků sestavený scénář nemusí být nutně nejkratším možným 19 * (takže to vlastně ani nemusí být základní úspěšný scénář), 20 * ale musí vyhovovat všem okrajovým podmínkám zadání, 21 * tj. musí obsahovat minimální počet kroků, 22 * projít požadovaný.minimální počet prostorů 23 * a demonstrovat použití všech požadovaných příkazů. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 60 z 240
Kapitola 3: Návrh správce scénářů konkrétní hry
61
24 */ 25 private static final ScenarioStep[] HAPPY_SCENARIO_STEPS = 26 { 27 START_STEP, 28 29 new ScenarioStep(tsMOVE, pJDI + " " + KOUPELNA, 30 zPŘESUN + KOUPELNA + 31 String.format(FORMÁT_INFORMACE, 32 KOUPELNA, 33 cm(PŘEDSÍŇ), 34 cm(BRÝLE, UMYVADLO, ČASOPIS), cm()), 35 36 KOUPELNA, 37 new String[] { PŘEDSÍŇ }, 38 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 39 new String[] {} 40 ), 41 42 new ScenarioStep(tsPICK_UP, pVEZMI + " " + BRÝLE, 43 zZVEDNUTO + BRÝLE + 44 String.format(FORMÁT_INFORMACE, 45 KOUPELNA, 46 cm(PŘEDSÍŇ), 47 cm(UMYVADLO, ČASOPIS), cm(BRÝLE)), 48 49 KOUPELNA, 50 new String[] { PŘEDSÍŇ }, 51 new String[] { UMYVADLO, ČASOPIS }, 52 new String[] { BRÝLE } 53 ), Jak jsem již naznačil, takto koncipovaný správce je trochu univerzálnější a lze jej snadněji lokalizovat. Proto se ve vzorovém programu vydám touto cestou.
3.7
Úkol
Takže po celé té předehře se konečně dostáváme k prvnímu skutečnému úkolu. Zkuste nyní vymyslet vlastní námět takovéto hry a definujte její úspěšný a chybový scénář. Omezující podmínky jsou následující:
Při postupu podle úspěšného scénáře musí hráč dosáhnout požadovaného cíle hry.
V prostorech musí být objekty, z nichž některé je možno zvednout a jiné zase ne. (Nemusí být ve všech prostorech.)
Hra musí pracovat s následujícími příkazy: Startovací příkaz, jehož název je prázdný řetězec. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 61 z 240
62
Návrh semestrálního projektu a jeho rámce – Adventura
Prostý přechod z prostoru do sousedního prostoru. Zvednutí objektu, tj. jeho odebrání z aktuálního prostoru a uložení v batohu.
Položení objektu, tj. jeho přesun z batohu do aktuálního prostoru. Předčasné ukončení hry. Nápověda. Vaše vlastní příkazy. Kroky, v nichž budou v testovacích scénářích tyto příkazy použity, budou typu tsNON_STANDARD.
V
úspěšném scénáři musí být použit povinný příkaz pro přesun a příkazy pro zvednutí a položení objektu.
Úspěšný
scénář musí mít jistý „minimální rozměr“ specifikovaný veřejnou konstantou AScenarioManager.LIMITS, která je instancí třídy Limits. Tento minimální rozměr se dozvíte např. tak, že konstantu vytisknete. Pak se můžete dozvědět např. že: Minimální požadované "rozměry" úspěšného scénáře: Minimální počet kroků = 10 Minimální počet prostorů hry = 6 Minimální počet navštívených prostorů = 4 Minimální počet vlastních, tj. nepovinných příkazů = 4
Hra musí být schopna se korektně vypořádat s nesprávně zadanými příkazy uživatele. V chybovém scénáři se proto musejí vyskytovat všechny typy kroků, jejichž konstruktoru je ve výčtovém typu TypeOfStep zadána hodnota 1 (tj. kroky s podtypem Subtype.stMISTAKE). V chybovém scénáři proto musíte prověřit správné reakce na:
Pokus o odstartování hry jiným než startovacím příkazem Zadání neexistujícího příkazu. Zadání prázdného příkazu uprostřed hry Zadání příkazů vyžadujících parametr bez tohoto parametru. Příkaz k přechodu do prostoru, který neexistuje anebo v daný okamžik není přístupným sousedem aktuálního prostoru.
Příkaz k zvednutí objektu, který se v aktuálním prostoru nevyskytuje. Příkaz k zvednutí nezvednutelného objektu. Příkaz k položení objektu, který nemáte v batohu. Příkaz ke zvednutí objektu, který se již nevejde do batohu. Kromě výše uvedených „povinných“ součástí můžete do svého návrhu vložit řadu dalších příkazů a nápadů. Fantazii a tvořivosti se meze nekladou.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 62 z 240
Kapitola 3: Návrh správce scénářů konkrétní hry
63
Náměty Pro vaši inspiraci uvádím několik nápadů tak, jak jsem je posbíral z odevzdaných studentských prací, v nichž jsem opravil pouze některé do nebo volající gramatické chyby.
Po prohýřené noci se probouzíš zamčený(ná) v cizím bytě. Nejprve musíš najít své svršky, pak telefonní číslo majitele bytu, jemuž zavoláš. Od něj se dozvíš, kde jsou klíče, aby ses dostal(a) ven.
Jsi před tajemným hradem, v němž je uvězněna princezna, kterou hlídá drak. Musíš získat zbroj, dostat se do hradu, najít a přemoci draka a pak najít a osvobodit princeznu.
Jsi myš. Tvým úkolem je dostat se i s dětmi do bezpečí spíže nedalekého domu a nenechat se přitom chytit a sníst kocourem.
Jsi člen týmu SG-2 a byl jsi poslán na akci na neznámou planetu. Prošel jsi s týmem hvězdnou bránou, ale něco se pokazilo. Dostal ses jinam než zbytek týmu. Probouzíš se v temné jeskyni. Rány ti ošetřuje medvědu podobný tvor a žvatlá něco nesrozumitelného. Asi jsi někde nechal překladač. Říkáš si: „Musím se nějak dostat zpět domů, ale neznám hvězdnou adresu na Zemi!“
Zrovna jste se vracel do kajuty, když se loď, na jejíž palubě jste, začala potápět. Musíte se dostat přes všechny překážky na palubu.
Vítej ve hře GeoCaching. Tvým úkolem je najít 3 části souřadnic, ze kterých vypočítáš souřadnice cílové oblasti. Abys ale hru dohrál, musíš tam položit 2 mince, které musíš najít cestou. Navíc se tam musíš podepsat, k čemuž budeš potřebovat pero.
Právě jsi v ložnici u své milenky. Najednou slyšíš, že domů přišel její manžel. Je čas jít domů a oslavit s manželkou výročí svatby.
Toto je nová, detektivní adventura. Vyřešte záhadnou krádež zlatých cihel. Byl jsi právě nespravedlivě odsouzen. Tvým úkolem je získat obnos k podplacení dozorců a uprchnout z vězení.
Jack sa jedno ráno zobudil na tom najdivnejšom mieste, na okraji malého lesíku, vôbec si nepamätal, ako sa tam dostal, vlastne ani nevedel že kde je. Pomôžete mu dostať sa domov?
Máš velké finanční problémy. Přitom tvůj dům sousedí s bankou, jejíž trezor je hned vedle tvého sklepa. Pokus se o víkendu banku vyloupit.
Tvé město obsazují turisté jako kobylky. Je třeba se jich jednou provždy zbavit. Není zbytí. Je třeba sehnat jelena a ukázat všem, kdo je tady pánem. Na město padne děs. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 63 z 240
64
Návrh semestrálního projektu a jeho rámce – Adventura
3.8
Co jsme prozatím naprogramovali
Udělejme si tedy souhrn toho, co jsme doposud naprogramovali:
Máme definovanou třídu správce scénářů – třídu OfficialApartmentManager. Protože jsme se rozhodli (dobrá, nedal jsem vám na výběr, já se rozhodl) používat ve scénářích konstanty a zjednodušit si tak opravy případných chyb, definovali jsme i třídu s textovými konstantami – třídu Texts.
Definovali jsme tovární třídu, jejíž instance fungují jako tovární objekty zprostředkující získání instancí správce scénářů a vlastní hry – třídu GSMFactory. jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A03z_ScenarioManager.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 64 z 240
65
Kapitola 4: Návrh rámce pro hru 4.
Návrh rámce pro hru
Kapitola 4 Návrh rámce pro hru
4.1
Co se v kapitole naučíme V této kapitole se pokusíme převést verbální (slovní) zadání hry z minulé kapitoly do programové podoby tak, aby se požadavky tohoto zadání pokud možno přeměnily do požadavků definovaných interfejsů, a pokud to jen trochu půjde, tak rovnou do signatur, které, jak víme, dokáže zkontrolovat již překladač. Požadavky, které zůstanou na úrovni kontraktu, bude muset následně plnit testovací program. I jeho práci se však pokusíme ulehčit.
Koncepce rámce hry
Prozatím ale žádnou třídu hry nemáme, takže musíme zvolit nějaké náhradní řešení. Nabízejí se nám dvě možnosti:
Jednodušším řešením je definovat prázdnou třídu nazvanou např. Hra a vracet odkaz na její instanci. Tuto třídu bychom postupně vylepšovali.
Druhou možností je nedefinovat přímo třídu hry, ale definovat pouze interfejs nazvaný např. IGame, který bude specifikovat základní vlastnosti připravované hry a který budou implementovat třídy realizující nějakou hru. K němu by bylo vhodné přidat interfejsy dalších objektů hry a získat tak jistý rámec (framework), jemuž budou vyhovovat všechny vytvářené hry. Druhé řešení je zdánlivě složitější. Samouci, kteří si chtějí připravit jedinou hru, mohou zvolit první řešení. Ve škole se však setkáte spíše s tím druhým, protože pak může každý student definovat svoji vlastní hru, a přesto bude možno všechny odevzdané hry zpracovávat jednotným způsobem. Navíc je při jeho aplikaci můžeme naučit několik dalších principů, které se nám budou v další praxi hodit.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 65 z 240
66
Návrh semestrálního projektu a jeho rámce – Adventura
Výhodou řešení se společným rámcem je i to, že bychom chtěli v dalším kroku navrhnout nějaké grafické uživatelské rozhraní našich her, mohly bychom je navrhnout tak, aby bylo možno s jeho pomocí spustit libovolnou hru vyhovující požadavkům našeho rámce. Jak jste jistě odhadli, přikloníme se k druhému řešení. Připravíme rámec, kterému budou muset vyhovovat naše hry i doprovodné scénáře. Nebude to výhodné pouze pro učitele, ale i pro vás. Když budete připravovat svoji vlastní hru, jistě oceníte, že nemusíte vyvíjet vše od začátku, ale že pro řadu pomocných činností (např. právě pro testování) budete moci využít služeb tohoto rámce.
4.2
Objekty vystupující ve hře
Pojďme se tedy zamyslet nad tím, jaké objekty budou v naší hře vystupovat, a jako interfejsy by proto měl náš rámec obsahovat. Jeden ze způsobů návrhu doporučovaný začátečníkům je vypsat si zadání a zvýraznit v něm všechny podstatné jména. To budou kandidáti na budoucí objekty a třídy objektů. Vezměme proto zadání z pasáže Koncepce hry z minulé kapitoly, vyjmeme z něj upřesňující věty a zvýrazníme v něm podstatná jména:
Zadání Hra probíhá ve virtuálním světě, v němž existuje několik prostorů, které spolu zadaným způsobem sousedí. Hra musí nabízet příkaz zprostředkující přechod z aktuálního prostoru do sousedního prostoru. V každém prostoru se mohou nacházet různé objekty. Některé z nich může hráč vzít, uložit je do pomyslného batohu, aby mu v budoucnu pomohly ke splnění nějakého pomocného úkolu nebo dokonce cíle celé hry. Hra musí proto nabízet příkaz, který přesune zadaný objekt z aktuálního prostoru (tj. prostoru, v němž se právě nachází hráč) do batohu a příkaz, která naopak přesune objekt z batohu do aktuálního prostoru. Množství objektů, které se do batohu vejdou, je však omezené. Příkaz pro přesun objektu z prostoru do batohu proto nesmí povolit překročení kapacity batohu. Zrovna tak nesmí umožnit uložit do batohu nezvednutelný objekt.
Definice použitých interfejsů Pojďme nyní projít ono zadání, provést analýzu nalezených podstatných jmen a rozhodnout, která z nich si zaslouží, aby je ve vytvářeném rámci zastupoval nějaký interfejs. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 66 z 240
Kapitola 4: Návrh rámce pro hru
67
Hra – IGame Prvním podstatným jménem je hra. Ta je určitě nezpochybnitelným kandidátem, protože ta je tím hlavním objektem, jehož vytvoření je naším cílem. Všechny ostatní objekty budou pouze pomocné. Definujeme proto interfejs IGame, jehož instance budou představovat vytvořenou hru. Svět – IWorld Dalším podstatným jménem je svět, v němž se bude celá hra odehrávat. Svět pak už dále v zadání nevystupuje, ale působí dojmem objektu, který by mohl mít v programu důležitou roli, a proto definujeme interfejs IWorld, jehož instance budou představovat světy jednotlivých her. Dopředu si ale zapamatujeme, že instance světa by měla být jedináček, protože hra musí probíhat pouze v jediném světě. Prostor – ISpace Následujícím podstatným jménem v zadání je prostor. Prostory, v nichž se hra odehrává, jsou její klíčovou součástí a hovoří se o nich i v zadání doplňujících podmínek pro tvorbu scénáře (hra musí obsahovat zadaný minimální počet prostorů). Je tedy více než zřejmé, že bude nanejvýš vhodné definovat interfejs ISpace, jeho instance budou představovat prostory dané hry. Příkaz – ICommand Při dalším čtení narazíme na podstatné jméno příkaz. Příkazy jsou dalšími klíčovými prvky hry, na které jsou kladeny dodatečné podmínky (existují povinné příkazy a minimální počet dodatečných příkazů, které je třeba definovat), takže pro ně definujeme jejich vlastní interfejs ICommand. Možná vám bude připadat divné, proč by měl být příkaz objektem. Uvědomte si ale, že jednotlivé příkazy budou uchovávat informace o tom, jak má hra zareagovat v případě, kdy hráč takový příkaz zadá. Přitom musejí umět rozumně zareagovat i v případě, kdy hráč zadá příkaz špatně. Špatně zadaný příkaz nesmí program zhroutit, ale pouze vyvolá reakci, která hráče upozorní na jeho chybu. Přechod Následují podstatné jméno přechod. Tady už to není tak jasné. Mohli bychom sice požadovat vytvoření objektu definujícího přechod z prostoru do prostoru, ale na druhou stranu bychom mohli pojmout otázku těchto přechodů tak, že to, jestli je možné z jednoho prostoru přejít do druhého, je interní věci daného prostoru, a že by proto tuto informaci měl spravovat daný prostor a neměla by být proto uložená v nějakém veřejném objektu. V zájmu zapouzdření bychom tedy speciální ve49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 67 z 240
68
Návrh semestrálního projektu a jeho rámce – Adventura
řejné objekty pro přechod z prostoru do prostoru nevytvářeli. O tom, jestli mají vůbec nějaké vzniknout, bychom rozhodli, až budeme vytvářet třídu definující prostory. Objekt – IObject Při dalším čtení přeskočíme několik prostorů, o nichž už jsme rozhodli, a narazíme na podstatné jméno objekt. Objekty jsou opět klíčovým prvkem hry s dalšími omezujícími podmínkami (některé musí jít zvednout a jiné ne), takže není pochyb o tom, že by měli mít v rámci svého zástupce – bude jím interfejs IObject. Hráč Následuje podstatné jméno hráč. Tady asi opět trochu zaváháme. Musíme si rozmyslet, jestli pro nás hráče představuje uživatel (případně testovací program), anebo jestli se jedná opravdu o objekt hry. Musíme pročíst zadání a ujasnit si, jaká by mohla být role hráče v programu. Ze zadání vyplývá, že hráč se někde nachází a má něco v batohu. V krocích scénáře, které jsme navrhovali, jsou aktuální prostor i obsah batohu samostatné informace. Je tedy otázkou, jestli je jako samostatné informace držet i v programu – jestli např. nedefinovat samostatný batoh s tím, že si pozici hráče bude pamatovat třeba svět, v němž se hráč nachází. Obě cesty jsou možné. Když jsem se o těchto možnostech rozhodoval já, dospěl jsem k závěru, že informace týkající se hráče jsou poněkud heterogenní (nesourodé) a takže by hráč buď dělal několik relativně různých věcí, anebo by fungoval jej jako přepravka pro informace získané specializovanými objekty. Rozhodl jsem se proto objekt hráče do hry nezavádět a využít pro získání informací o hráči specializované objekty: pro informaci o aktuálním prostoru svět a pro informaci o obsahu batohu speciální objektu reprezentující pouze batoh. Batoh – IBag Při dalším čtení zadání narazíme vzápětí na podstatné jméno batoh. O něm jsem před chvílí hovořil, takže je vám jistě jasné, že je o něm rozhodnuto: bude jej reprezentovat samostatný objekt – instance interfejsu IBag. Úkol, cíl V následujícím textu se hovoří o pomocném úkolu a o cíli celé hry. Tyto pojmy jsou však evidentně pouze doplňkové a není třeba pro ně v rámci definovat nějakou speciální reprezentaci.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 68 z 240
Kapitola 4: Návrh rámce pro hru
69
Množství, kapacita Následuje řada podstatných jmen, které se v textu již vyskytly, a na jejich výskyt jsme již zareagovali. Poslední doposud neprodiskutovaná podstatná jména jsou množství objektů v batohu a kapacita batohu. Obě označují totéž, přičemž se asi shodneme, že se jedná o interní informaci batohu, kterou by si měl batoh spravovat sám a není třeba pro ni definovat nějaký zvláštní objekt.
4.3
Upřesnění návrhu rámce
Z předchozí analýzy nám vyplynuly interfejsy, které by měly v rámci reprezentovat objekty, které by bylo vhodné v každé hře vytvořit. Každá hra si je vytvoří po svém, ale v rámci budou jejich společní rodiče definující, jaké vlastnosti mají tyto objekty mít. Pojďme si je tedy ještě jednou shrnout: IGame
Hra
IWorld
Svět, v němž hra probíhá
ISpace
Prostory, mezi nimiž hráč přechází
IObject Objekty v prostorech IBag
Batoh, přenášené objekty
ICommand Příkazy zadávané uživatelem Pojďme nyní projít jeden interfejs po druhém a ujasněme si, co budeme vyžadovat po objektech, které daný interfejs implementují. Musíme si přitom uvědomit, že připravujeme rámec, který má na jedné straně poskytnout programátorům (= studentům) jakési mantinely, v nichž se budou pohybovat, a na druhou stranu poskytnout našemu rámci prostředky, aby mohl obdržená řešení otestovat. Naše požadavky proto nebudou řešit otázku, jak danou hru co nejlépe naprogramovat, ale prozatím jen jak danou hru co nejlépe otestovat. Začněme od konce našeho seznamu.
ICommand Abychom poznali, který příkaz uživatel zadal, a která z příkazových objektů máme proto pověřit realizací příslušné reakce, musíme příkazy pojmenovat. Po příkazech proto budeme chtít implementaci metody String getName() Měli bychom si uvědomit, že jméno příkazu musí být jedinečné, a to i tehdy, když při porovnávání jmen příkazů nebude záležet na velikosti písmen. Teoreticky by49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 69 z 240
70
Návrh semestrálního projektu a jeho rámce – Adventura
chom měli do programu zakomponovat požadavek na kontrolu jedinečnosti těchto jmen. Je nám ale jasné, že tento požadavek se budou tvůrci snažit dodržet i bez nabádání, protože kdyby programátor tento požadavek nedodržel, program by se mu buď výrazně zkomplikoval, anebo by vůbec nechodil. Mezi povinnými příkazy je i příkaz k realizaci nápovědy. Ten by měl uživateli stručně popsat vlastnosti jednotlivých příkazů. Nejlepší by bylo, kdyby každý příkaz znal svůj stručný popis a uměl jej vrátit jako reakci na zaslání příslušné zprávy. Budeme proto požadovat definici metody String getDescription() Objekty příkazů slouží k reakcím na příkazy uživatele. Měly by tedy implementovat příslušnou metodu, která zpracuje uživatelův příkaz a vrátí text, který hra vypíše jako reakci na uživatelův příkaz. Při deklaraci požadavku na danou metodu musíme mít na paměti, že některé příkazy budou vyžadovat parametry – např. při přesunu je třeba zadat prostor, do nějž se má hráč přemístit. Musíme proto umět tyto parametry dané metodě předat. Nemá smysl, aby každý příkaz uměl analyzovat vstup uživatele. Příslušný kód umístíme do jednoho místa (které to bude, určíme až při implementaci) a tento kód rozhodne, který příkaz uživatel zadal. Při té příležitosti by mohl také zjistit, jaké zadal parametry a předat je onomu příkazu v poli řetězců, jehož prvky by byla jednotlivá slova z příkazového řádku zadaného uživatelem. Z toho vyplývá, že signatura potřebné metody by mohla být String execute(String[] parameters)
IBag Zadání vyžaduje, aby batoh měl konečnou kapacitu. Budeme proto po batohu požadovat implementaci metody int getCapacity() Kromě toho bychom potřebovali vědět, jaké objekty jsou v dané chvíli v batohu, abychom mohli zkontrolovat, že se po příkazu požadujícím přidání objektu do batohu daný objekt v batohu opravdu objevil. Do interfejsu proto přidáme deklaraci metody Collection extends IObject> getObjects() Připomínám, že tato deklarace vyžaduje, aby metoda vrátila kolekci objektů, které jsou instancemi třídy implementující interfejs IObject.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 70 z 240
Kapitola 4: Návrh rámce pro hru
71
IObject Každý objekt v prostoru se musí nějak jmenovat, protože jinak bychom nemohli rozumně určit, se kterým z nich chceme pracovat. Budeme proto po něm vyžadovat implementaci metody se signaturou String getName() V zadání je navíc uvedeno, že některé objekty lze zvednou a přemístit do batohu a některé ne. Každý objekt by měl proto vědět, jestli je možno jej zvednout. Tento požadavek můžeme zobecnit a požadovat po objektech aby znaly svoji váhu. Program se tím nijak nezesložití, a přitom bude možno vytvářet hry, v nichž bude počet objektů, které se vejdou do batohu, záviset na tom, kolik tyto objekty váží. Kdo bude chtít zůstat pouze u počtu objektů, může přenositelným objektům přiřadit váhu 1 a nepřenositelným váhu větší, než je kapacita batohu. Budeme proto po objektech požadovat implementaci metody int getWeight()
ISpace I prostory budou muset mít svá jména, abychom mohli určit, do kterého z nich se chceme přesunout. I pro ně budeme proto vyžadovat implementaci metody String getName() V zadání je navíc uvedeno, že přesouvací příkaz povoluje přesun pouze do sousedního prostoru. V krocích scénáře je vždy uvedeno, se kterými prostory daný prostor v daném okamžiku sousedí. Abychom to mohli zkontrolovat, musí každý prostor umět prozradit své aktuální sousedy. Budeme po něm proto požadovat implementaci metody Collection extends IArea> getNeighbors() Navíc víme, že v prostorech se mohou vyskytovat objekty, z nichž některé může uživatel zvednout. I ty se v krocích scénáře uvádějí, takže je potřebujeme mít možnost zkontrolovat. Budeme proto po prostorech požadovat implementaci metody Collection extends IObject> getObjects()
IWorld Objekt představující svět naší hry bude muset na počátku hry tento svět nějak zorganizovat, tj. vytvořit jednotlivé prostory a vzájemně propojit ty sousední. Měl
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 71 z 240
72
Návrh semestrálního projektu a jeho rámce – Adventura
by tedy sloužit jako takový správce prostorů hry, který má o těchto prostorech přehled. Vlastní vytváření a vzájemné propojování jednotlivých prostorů kontrolovat nebudeme, ale ponecháme si možnost zkontrolovat výsledek. Budeme proto požadovat implementaci metody Collection extends IArea> getAllAreas() která vrátí všechny prostory, které se v daném okamžiku ve hře vyskytují nezávisle na tom, jestli jsou hráči přístupné či ne. Když bude tento objekt mít na starosti správu všech prostorů, mohli bychom po něm v průběhu chtít, aby nám prozradil, ve kterém prostoru se v daném okamžiku nachází hráč, abychom to mohli porovnat s tím, co se dozvíme v příslušném kroku scénáře. Budeme proto požadovat implementaci metody IArea getCurrentArea()
IGame A jsme ve finále – budeme se rozhodovat o tom, jaké požadavky budeme klást na každou hru. Začnu tím, že si uvědomíme, že se jedná o úlohu určenou k hodnocení, a proto budeme požadovat, aby nám prozradila, kdo je jejím autorem a komu tedy připsat příslušné hodnocení. Budeme postupovat stejně jako u správce scénářů a definujeme interfejs hry jako potomka interfejsu IAuthor, který vyžaduje implementaci metod public final String getAuthorName() public final String getAuthorID() Dále by nám hra měla pro účely testování poskytnout objekty, o nichž jsme hovořili v předchozích pasážích. Budeme proto po ní chtít, aby implementovala metody: Collection extends ICommand> getAllCommands() IBag getBag() IWorld getWorld() Tím bychom měli získat kompletní přehled o jejím aktuálním stavu. Jenomže k tomuto přehledu nám ještě něco chybí, a to informace o tom, jestli hra zrovna běží, anebo na své spuštění teprve čeká. Přitom je jedno, jestli se jedná o její první spuštění, anebo už ji někdo hrál, ukončil a ona čeká na další spuštění. K tomuto účelu požádáme hry o implementaci metody boolean isAlive() která bude vracet true případě, že hra je právě hraná, a false případě, kdy na své spuštění čeká. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 72 z 240
73
Kapitola 4: Návrh rámce pro hru
Abychom mohli hru korektně otestovat, musíme umět získat správce scénářů, který obsahuje všechny informace o plánovaném chování hry. Požádáme proto hru o implementaci metody AScenarioManager getScenarioManager() která vrátí odkaz na správce jejích scénářů, podle nichž bude možno danou hru otestovat. Vrátím se ještě k zadání. To říká, že hra má implementovat sadu šesti povinných příkazů (spuštění hry, přesun do zadaného sousedního prostoru, zvednutí a položení objektu, nápovědu a předčasné ukončení). Zadaný název má ale jen příkaz pro spuštění hry. Názvy ostatních příkazů jsou zcela na libovůli tvůrce dané hry. Kdybychom znali názvy těchto příkazů, mohli bychom leccos otestovat i bez znalosti scénářů. Doplníme proto interfejs o požadavek na definici metody, která vrátí názvy zbylých pěti povinných příkazů: Commands getBasicCommands() Metoda vrací přepravku typu Commands, jejíž konstruktor má rozhraní: /*************************************************************************** * Vytvoří přepravku uchovávající názvy příkazů, * které musí být implementovány ve všech hrách. * Názvy těchto příkazů musí být jednoslovné * stejně jako jejich případné parametry. * * @param move Název příkazu pro přesun z místnosti do místnosti * @param putDown Název příkazu pro položení objektu * @param pickUp Název příkazu pro zvednutí objektu * @param help Název příkazu pro získání nápovědy * @param end Název příkazu pro ukončení hry */ public Commands(String move, String putDown, String pickUp, String help, String end) Požadavek, který jsme ještě nevzali zcela do úvahy, byla existence příkazu pro předčasné ukončení hry. Když má mít tuto možnost hráč, měl by ji mít i program, který s hrou komunikuje. Doplníme proto interfejs o deklaraci metody void stop() jejímž zavoláním předčasně ukončíme případnou rozjetou hru. Celou dobu, co se bavíme o interfejsu IGame, hovoříme pouze o podpůrných metodách. Zatím jsme ale nevzali do úvahy hlavní účel hry: akceptovat příkazy hráče a posílat mu zprávy popisující reakce programu. To teď na závěr napravíme a doplníme interfejs o deklaraci metody String executeCommand(String command) 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 73 z 240
74
Návrh semestrálního projektu a jeho rámce – Adventura
Ta převezme příkaz uživatele, nechá jej zpracovat a vrátí textový řetězec s odpovědí uživateli.
4.4
Doplnění společných rodičů
Projděme si nyní naše předchozí požadavky a zamysleme se nad tím, jestli bychom nemohli pro některé z navrhovaných datových typů definovat společného rodiče. Při bližším zkoumání uvidíme dva skupiny kandidátů.
Pojmenované objekty Příkazy, prostory i objekty v prostorech musejí mít přiřazena jména. Všechny tří interfejsy bychom proto mohli definovat jako potomky společného rodiče, kterého můžeme nazvat INamed a který bude deklarovat metodu String getName() To nám umožní definovat metody, jejichž činnost bude nějak souviset s názvem objektu a které by mohly pracovat s libovolným pojmenovaným objektem. V tomto interfejsu bychom např. mohli definovat statické metody, které se pokusí v zadané kolekci, poli či proudu najít objekt se zadaným názvem.
Kontejnery objektů Batoh i prostory mohou obsahovat objekty. Oba bychom tedy mohli prohlásit za speciální případy obecného kontejneru objektů a definovat pro ně společného rodiče IObjectContainer, který bude deklarovat jejich společnou metodu Collection extends IObject> getObjects()
4.5
Doplnění uživatelského rozhraní
Zatím jsme uvažovali pouze o vlastní hře. Jenomže aby se taková hra mohla hrát, je potřeba definovat i uživatelské rozhraní, které zprostředkuje komunikaci programu s uživatelem. Tak, jako jsme definovali požadavky na vytvářenou hru, měli bychom definovat požadavky i na uživatelské rozhraní, které umožní hrát danou hru i řadovému uživateli (a nejen našemu testovacímu programu). Pro začátek nebudeme přemýšlet o tom, jak bychom mohli správnou funkci vytvořeného uživatelského rozhraní automaticky otestovat, a budeme požadovat 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 74 z 240
Kapitola 4: Návrh rámce pro hru
75
pouze to, aby umělo danou hru spustit. V dalším kole mu možná přidáme i nějakou inteligenci, kterou bychom mohli otestovat, ale to prozatím necháme na ono případné další kolo a zůstaneme u požadavku na spuštění hry. Budeme ale chtít, aby toto rozhraní umělo spustit libovolnou vyhovující našemu rámci, takže po něm budeme požadovat implementaci dvou metod: první bude spouštět hru zadanou v parametru a druhá bude bezparametrická a bude spouštět vlastní hru autora. Budeme v něm tedy deklarovat metody se signaturami: void startGame() void startGame(IGame game) Lze přitom očekávat, že bezparametrická verze pouze získá instanci hry vytvořené autorem rozhraní a předá ji v parametru jednoparametrické verzi. Pokud ale autor hry přidal do svého výtvoru nějaké speciální funkce překračující povinnou funkčnost požadovanou rámcem (frameworkem), může definovat uživatelské rozhraní tak, aby v případě, že se hraje jeho hra, využívalo jejích nadstandardních vlastností.
4.6
Diagram tříd balíčku game_txt
Všechny interfejsy rámce hry, o nichž jsme v předchozím textu hovořili, jsou definovány v balíčku game_txt, přesněji cz.pecinovsky.adv_framework.game_txt. Jednoduchý diagram tříd tohoto balíčku si můžete prohlédnout na obrázku 4.1.
Obrázek 4.1 Jednoduchý diagram tříd balíčku game_txt
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 75 z 240
76
4.7
Návrh semestrálního projektu a jeho rámce – Adventura
Diagram tříd v balíčku empty_classes
Z předchozího je zřejmé, že jednotlivé hry se sice budou lišit, ale na druhou stranu v jejich definicích bude také hodně společného. Známe-li rámec hry, můžeme téměř s jistotou předpokládat, jaké třídy v budoucích projektech vzniknou. Budou se sice různě jmenovat, ale budou mít hodně společného. Pro usnadnění vývoje studentských aplikací je proto v rámci připraven balíček cz.pecinovsky.adv_framework.empty_classes, v němž jsou definovány třídy, které mohou sloužit jako kostry jednotlivých tříd, které budou součástí vytvářené aplikace. Jednoduchý diagram tříd tohoto balíčku si můžete prohlédnout na obrázku 4.2.
Obrázek 4.2 Jednoduchý diagram tříd balíčku empty_classes
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 76 z 240
77
Kapitola 5: Začínáme tvořit vlastní hru 5.
Začínáme tvořit vlastní hru
Kapitola 5 Začínáme tvořit vlastní hru
5.1
Co se v kapitole naučíme V této kapitole si ukážeme, jak lze vyvinout aplikaci, která by odpovídala rámci navrženému v kapitole minulé. Současně si ukážeme, jak při tomto vývoji využít toho, že už máme předem připravené testy.
Výběr metody návrhu
Jak už jsme si říkali v základním kurzu, při návrhu programu můžeme obecně postupovat dvěma způsoby:
Při návrhu metodou shora dolů (top-down design), při němž nejprve navrhneme hlavní program (byť zpočátku nebude zcela funkční), a ten pak postupně doplňováním jednotlivých chybějících částí upřesňujeme a zdokonalujeme, až nakonec získáme kompletní řešení.
Při návrhu metodou zdola nahoru (bottom-up design) naopak nejprve vytvoříme základní stavební kameny, z nich pak vytvoříme větší a větší celky, až nakonec získáme celý výsledný program.
Každý z uvedených postupů má své výhody a nevýhody, takže se občas zvolí hybridní metoda, při níž postupujeme chvíli shora dolů a chvíli zdola nahoru tak, abychom maximálně využili výhod každé z těchto metod a naopak pokud možno eliminovali její nevýhody. Návrh adventury postupem zdola nahoru si můžete prohlédnout na mých stránkách s animacemi. Koncepce rámce sice tehdy byla maličko jiná, ale bylo to opravdu jen maličko.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 77 z 240
78
Návrh semestrálního projektu a jeho rámce – Adventura
Protože jsem postup zdola nahoru již předváděl, zkusíme to nyní právě obráceně: půjdeme shora dolů. Navrhneme třídu celé hry a k ní budeme postupně přidávat součásti tak, jak nás o to bude žádat testovací program.
5.2
Začínáme s třídou hry
Začneme vytvářet vlastní třídu hry. Můžete vše sledovat a zkoušet realizovat ekvivalentní kód i ve své aplikaci. Začneme tím, že z balíčku empty_classes zkopírujeme třídu EmptyGame, která slouží jako šablona třídy hry, a hned ji přejmenujeme tak, aby název třídy odpovídal tématu hry. Moje demonstrační hra se odehrává ve služebním bytě, a proto třídu nazvu OfficialApartmentGame. Zkopírovaná a přejmenovaná třída se otevře v NetBeans a já ji hned upravím úvodní dokumentační komentář tak, abych z něj smazal texty týkající se pouze šablony a vhodně upravil texty týkající se vytvářené třídy. Navíc hned smažu automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Současně přejmenuji třídu správce scénářů tak, aby „ladila“ s názvem třídy hry – pojmenuji ji OfficialApartmentManager. Abyste se v programu vyznali, doporučuji vám se zachovat obdobně. Nebudeme se nyní snažit nějak bádat nad tím, co bychom měli a neměli naprogramovat nejdříve a rozhodneme se, že pojedeme přesně podle metodiky TDD a necháme se dirigovat testy. Musíme začít tím, že otevřeme třídu správce scénářů a do konstanty CLASS přiřadíme odkaz na class-objekt své hry. Já jí přiřadím hodnotu OfficialApartmentGame.class, vy jí přiřadíte class-objekt své třídy hry. Zaběhneme ještě na konec zdrojového kódu a v metodě main(String[]) zakomentujeme příkaz na spuštění autotestu a odkomentujeme následující příkaz na spuštění testu hry. Od této chvíle můžeme začít svoji vyvíjenou hru testovat. První spuštění testu hry skončí podle očekávání vyhozením výjimky. Z výpisu se dozvíme: java.lang.UnsupportedOperationException: Metoda ještě není hotova. .at cz.pecinovsky.adventure15j.OfficialApartmentGame.getAuthorName(OfficialApartmentGame.java:89) at cz.pecinovsky.adv_framework.test_util.comon.Triumvirate.verifyTriumvirate(Triumvirate.java:202) atd
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 78 z 240
Kapitola 5: Začínáme tvořit vlastní hru
79
A je to jasné: ve třídě hry je třeba opravit definici metody getAuthorName, která vrací jméno autora. Při té příležitosti by vás mohlo napadnout, že bychom měli současně opravit i definici metody getAuthorID. Učiňme tak. Protože víme, že podobné metody jsou definovány i ve správci scénářů (přesněji v jeho rodiči), mohli bychom se rozhodnout je nedefinovat podruhé (víme, že opakování stejného či podobného kódu je programátorský hřích), ale delegovat poskytnutí požadované informace na příslušné dříve definované metody správce scénářů. Pak budeme vědět, že když si zadavatel později vzpomene s nějakou změnou (např. bude chtít prohodit pořadí jména a příjmení), stačí nám udělat opravu na jednom místě. Má to jednu drobnou vadu: potřebujeme oslovit svého správce scénářů. Můžeme požádat třídu správce o její instanci, ale když si uvědomíme, že zanedlouho budeme muset definovat metodu getScenarioManager(), bude lepší, když přestěhujeme žádost o instanci do této metody a požadavek na získání instance předáme této metodě. Důvod je stejný jako minule: kdyby se později změnilo zadání a instance správce scénářů se měla získávat jinak, budeme opět realizovat požadovanou úpravu na jediném místě. Když už budeme upravovat metodu pro získání správce scénářů, mohli bychom současně upravit její hlavičku a místo návratového typu AScenarioManager zadat OfficialApartmentManager. Náš správce je potomkem požadovaného typu, takže touto změnou požadavek implementovaného interfejsu nenabouráváme. Jenom jej zpřísňujeme, což smíme. Mohlo by se stát, že si tak někdy v budoucnu ušetříme nějaké to přetypování. Ve své třídě hry tedy již mám definovány první tři metody – jejich definice (bez dokumentačních komentářů) si můžete prohlédnout ve výpisu 5.1. Výpis 5.1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Definice metod getAuthorName(), getAuthorID() a getScenarioManager() ve třídě hry
@Override public String getAuthorName() { return getScenarioManager().getAuthorName(); } @Override public String getAuthorID() { return getScenarioManager().getAuthorID(); } @Override public OfficialApartmentManager getScenarioManager()
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 79 z 240
80
Návrh semestrálního projektu a jeho rámce – Adventura
17 { 18 19 }
5.3
return OfficialApartmentManager.getInstance();
Pokračujeme v testech
Po provedené úpravě znovu spustíme třídu správce scénářů a tím pádem i test hry. Tentokrát se dozvíme, že java.lang.UnsupportedOperationException: Metoda ještě není hotova. at cz.pecinovsky.adventure15j.OfficialApartmentGame.isAlive(OfficialApartmentGame.java:117) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyIsReady(GameTRunTest.java:406) atd Potřebujeme tedy definovat metodu, která žadateli prozradí, jestli je hra běžící, anebo jenom čekající na spuštění. Prozatím je hra ve výrobě, tak zvolíme cestu nejmenšího odporu a definujeme metodu tak, že bude vracet vždy false oznamující, že hra zatím neběží. Až se dostaneme do stavu, že hru spustíme, upravíme i tuto metodu. Definice metody isAlive() proto bude vypadat podle výpisu 5.2. Výpis 5.2: 1 2 3 4 5
Definice metod isAlive() ve třídě hry
@Override public boolean isAlive() { return false; }
Znovu spustíme správce scénářů a znovu vše skončí vyhozením výjimky – tentokrát nám výpis obsahu zásobníku prozrazuje následující: Ukončení bylo způsobeno vyhozením výjimky: cz.pecinovsky.adv_framework.test_util.comon.TestException Při vykonávání příkazu: «» vyhodila hra výjimku: at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyScenarioStep(GameTRunTest.java:302) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.executeScenario(GameTRunTest.java:202) atd Z výpisu vyčteme, že test už chtěl spustit startovací příkaz, tj. příkaz, jehož názvem je prázdný řetězec. (Aby se snadno poznalo, jestli zobrazovaný text není prázdný řetězec, uzavírají jej testy našeho rámce do francouzských uvozovek [např. «text»]. Prázdný řetězec se pak zobrazí «».)
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 80 z 240
81
Kapitola 5: Začínáme tvořit vlastní hru
Podíváme-li se do výpisu zásobníku dál, zjistíme, že se toho zase tolik moc nezměnilo, protož jako příčina výše uvedené výjimky je na konci uvedeno: Caused by: java.lang.UnsupportedOperationException: Metoda ještě není hotova. at cz.pecinovsky.adventure15j.OfficialApartmentGame.executeCommand(OfficialApartmentGame.java:207) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyScenarioStep(GameTRunTest.java:299) ... 6 more A už je to jasné – musíme definovat metodu executeCommand(String).
5.4
Kdo má mít na starosti zpracování příkazů
Tady se ale musíme zamyslet: opravdu by příkaz měla zpracovávat třída hry?
Tři skupiny programů Měli bychom si uvědomit, že objekty, které se vyskytující se v programech, bychom mohli rozdělit do tří skupin:
Objekty, jejichž hlavním úkolem je oprašovat nějaká data. Sem bychom mohli zařadit např. různé kontejnery (přepravky, pole, kolekce, …).
Objekty, jejichž hlavním úkolem je něco spočítat nebo jinak zpracovat. Sem bychom mohli zařadit např. přesouvač z našich hrátek s autíčky.
Objekty, jejichž hlavním úkolem je organizovat spolupráci ostatních objektů. Jestli si vzpomínáte na návrhový vzor Prostředník, tak to je takový typický organizátor, který zprostředkovává komunikaci ostatních objektů. V každé jenom trochu větší aplikaci by měl být nějaký organizátor. V naší aplikaci si o roli hlavního organizátora říká třída hry. Měli bychom ji proto definovat tak, aby se soustředila na delegování různých činností na specializované objekty.
Jak jsou na tom příkazy? Zpracování příkazů bude v naší hře důležitá činnost, kterou by měl mít na starosti nějaký specializovaný objekt. Tento objekt by se pak mohl současně starat o jednotlivé příkazy a spravovat je. Počítejte s tím, že jakmile se v programu vyskytne nějaké větší množství objektů, tak by v něm měl existovat i nějaký správce, který se o tyto objekty stará. Pro tuto činnost bychom měli v naší aplikaci definovat specializovaného správce příkazů. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 81 z 240
82
Návrh semestrálního projektu a jeho rámce – Adventura
Pojďme se proto podívat do rámce, co vše by naše příkazy měly umět, a pokusme se z toho odvodit požadavky na jejich správce. Víme, že každý příkaz má umět prozradit svůj název a popis. Kromě toho musí umět realizovat požadovanou činnost. Realizovaná činnost se příkaz od příkazu podstatně liší. Prozrazování názvu a popisu je však pro všechny stejné. Bylo by proto vhodné je delegovat na nějakého společného rodiče všech příkazů. Tomu by každý příkaz prozradil svůj název a popis a definici metod, které tyto informace na požádání vracejí, by ponechal na tomto společném rodiči. Definice by tak mohla být na jediném místě a všechny příkazy by tyto metody sdílely. Z principu dědění vyplývá, že při tvorbě každého příkazu by se musel nejprve vyrobit jeho rodičovský podobjekt. Všechny příkazy by tedy volaly společný konstruktor rodičovského podobjektu. To je přesně to místo, kde by se případný správce mohl o objektech, jejichž správu má na starosti, dozvědět vše potřebné hned při jejich vzniku. Od těchto úvah je již krůček k tomu, aby se správcem příkazů stal objekt jejich rodičovské třídy. Jinými slovy, aby rodičovská třída spravovala všechny vytvořené instance svých potomků. Třída si zřídí nějaký kontejner, do nějž bude ukládat vše, co bude o svých svěřencích potřebovat vědět. Rodičovský konstruktor příkazů, který je jednou z jejích metod, tak může jednoduše pověřit tím, aby kontejner naplnil potřebnými údaji.
5.5
Definice správce příkazů
Po těchto úvahách půjdeme definovat třídu ACommand, která bude rodičovskou třídou všech příkazů. Naštěstí pro nás má framework její šablonu připravenou v balíčku empty_classes. Zkopírujeme tedy do svého balíčku třídu EmptyACommand a hned ji přejmenujeme – odstraníme z jejího názvu slovo Empty (za chvíli beztak prázdná nebude). Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Při prohlídce zdrojového kódu naší nové třídy uvidíte, že třída není definována jako veřejná. Je to proto, že slouží pouze pro vnitřní potřebu hry, a nikdo cizí se na proto nemá mít možnost obracet. Třída má již definovaný konstruktor, který od svých potomků převezme jejich název a popis. Metody getName() a getDescription(), které název a popis příkazu na požádání vracejí, jsou prozatím poloprázdné. Nicméně doplnění jejich správných 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 82 z 240
Kapitola 5: Začínáme tvořit vlastní hru
83
těl je jen formalita. Tak hned ta těla opravíme. (Oprava je tak jednoduchá a jasná, že ji tu ani neukazuji.)
V učebnici jsme to probírali několikrát, ale některé věci je třeba občas připomenout. Některé studenty mate skutečnost, že instance neveřejné (a někdy dokonce i soukromé) třídy má veřejné metody. Je to proto, že tyto instance nevystupují vůči okolí jako instance dané třídy, ale jako instance interfejsu implementovaného jejich mateřskou třídou. Jako takové proto musejí mít definovány veřejné metody požadované daným interfejsem.
V definici zkopírované třídy je ještě abstraktní metoda s hlavičkou abstract public String execute(String... arguments); zděděná od implementovaného interfejsu ICommand. O tu se nyní starat nebudeme, o její definici se musejí postarat potomci, každý po svém. Nad definicí této metody začneme přemýšlet až v okamžiku, kdy budeme definovat jednotlivé příkazy. Teoreticky by tu tato deklarace vůbec nemusela být – abstraktní třída nemusí deklarovat abstraktní metody zděděné od svých předků, pokud se je rozhodne neimplementovat. Deklarace zděděné, neimplementované abstraktní metody je však užitečnou připomínkou toho, že tato metoda na svoji definici teprve čeká a že ji všichni konkrétní potomci musejí definovat.
Úprava definice konstruktoru Vraťme se však ještě ke konstruktoru. Zde bychom měli vše připravit k tomu, aby objekt třídy mohl pracovat jako správce instancí své třídy. Pojďme se tedy zamyslet nad tím, co k tomu bude potřebovat. Začneme odhadem jeho budoucí činnosti. 1. Vše začne tím, že uživatel napíše text příkazu. 2. Tento text bude potřeba rozdělit na slova. 3. První slovo příkazu bude klíčové. Bude oznamovat, který příkaz chce uživa-
tel provést. Budeme proto muset aktivovat objekt-příkaz, který se postará o provedení daného příkazu. Z předchozího vyplývá, že bychom měli mít k dispozici nástroj, do kterého hodíme název příkazu a vypadne z něj objekt-příkaz, který bude mít na starosti zabezpečení té správné reakce. Takovýto nástroj ale známe – je jím mapa. Klíčem bude název příkazu a hodnotou pak bude odkaz na objekt daného příkazu. Ve třídě proto definuji statickou konstantu NAME_2_COMMAND, který v deklaraci hned inicializuji. Mohu jej definovat jako konstantu, protože se sice bude měnit jeho obsah, ale vlastní mapu nikdo 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 83 z 240
84
Návrh semestrálního projektu a jeho rámce – Adventura
vyměňovat nebude. (Je to stejné, jako když chodíte s taškou na nákup: nákup se mění, ale taška zůstává stále táž.) Konstruktor pak do této mapy při každé konstrukci nového objektu uloží název objektu jako klíč spolu s odkazem na tento objekt jako hodnotou sdruženou s daným klíčem. Při práci s názvy však musíme dávat pozor na velikost písmen. Lze odhadnout, že někteří uživatelé budou při zadávání názvů příkazů dávat přednost malým písmenům a jiní velkým. Měli bychom se proto rozhodnout o podobě klíče a tomu pak později přizpůsobit zadávání a vyhledávání těchto názvů. Já jsem se rozhodl, že v klíčích budu používat pouze malá písmena. Před uložením klíče do mapy jej proto nejprve převedu. Pro vložení dvojice [název, příkaz] do mapy proto použiji příkaz NAME_2_COMMAND.put(name.toLowerCase(), this); Tato definice ještě není dokonalá. Dokumentace totiž říká, že při vložení dvojice [klíč; hodnota] do mapy vrátí metoda put() minulou hodnotu přiřazenou danému klíči. Vrátí-li null, je vše v pořádku. Vrátí-li něco jiného, znamená to, že jsme někde udělali chybu a snažíme se do mapy vložit druhý příkaz se stejným názvem. Definici bychom proto měli upravit tak, aby při vložení již existujícího názvu vyhodila výjimku. Definici příslušného statického atributu spolu s definicí upraveného konstruktoru si můžete prohlédnout ve výpisu 5.3. Výpis 5.3: 1 2 3 4 5 6 7 8 9 10 11 12 13
Upravená definice konstruktoru instancí třídy ACommand
private static final Map<String, ACommand> NAME_2_COMMAND = new HashMap<>(); ACommand(String name, String description) { this.name = name; this.description = description; ACommand put = NAME_2_COMMAND.put(name.toLowerCase(), this); if (put != null) { throw new IllegalArgumentException( "\nPříkaz s názvem «" + name + "» byl již vytvořen"); } }
Vytvoření jednotlivých příkazů Konstruktor jsme sice připravili, ale prozatím jsme neuvažovali o tom, kdo jednotlivé příkazy vytvoří, tj. kdo zkonstruuje objekty jednotlivých příkazů. Nejlepším vykonavatelem takovéhoto úkolu by měl být jejich pozdější správce.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 84 z 240
85
Kapitola 5: Začínáme tvořit vlastní hru
Před chvílí jsme si ale řekli, že správcovstvím pověříme objekt třídy ACommand. Nyní je třeba se rozhodnout, kdy se tento objekt o vytvoření příslušných příkazů postará a jak to naprogramujeme. Nejlepší by bylo, kdyby se o vytvoření jednotlivých spravovaných objektů postaral už při svém vzniku, aby jej pak o to později nemusel nikdo žádat. Jinými slovy, definice jednotlivých příkazů by měla být součástí konstruktoru daného objektu. A protože je tento objekt objektem třídy, umístíme vytváření nových příkazů do konstruktoru třídy ACommand, tj. do jejího statického inicializačního bloku.
Úprava metody execute(String) ve třídě hry Základní koncepci máme navrženu, takže nyní bychom měli vrhnout na metodu řešící problém koho pověřit provedením právě zadaného příkazu. Vrátíme se proto do třídy hry, kde definujeme tělo metody execute(String) tak, že deleguje zodpovědnost za reakci na správce příkazů, tj. na třídu ACommand. Metoda ve třídě hry tedy získá podobu z výpisu 5.4. Výpis 5.4: 1 2 3 4 5 6 7 8 9 10 11 12 13
Definice metody execute(String) ve třídě OfficialApartmentGame
/*************************************************************************** * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. * Vlastní zpracování příkazu ale deleguje na správce příkazů, * kterým je objekt třídy {@link ACommand}. * * @param command Zadávaný příkaz * @return Textová odpověď hry na zadaný příkaz */ @Override public String executeCommand(String command) { return ACommand.executeCommand(command); }
5.6
Definice metody pro provádění příkazů
Překladač prozatím ještě třídu hry nepřeloží, protože právě definovaná metoda execute(String) volá metod, která ještě neexistuje. Jdeme to tedy napravit. Abychom měli život co nejjednodušší, zkopírujeme definici metody za třídy hry do třídy ACommand. To ale překladači stačit nebude, protože jsme zkopírovali instanční metodu a příkaz na řádku 12 ve výpisu 5.4 jasně volá metodu třídy. Abychom jej uklidnili, přidáme zkopírované metodě modifikátor static. Tím z ní uděláme metodu třídy a překladač se uklidní. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 85 z 240
86
Návrh semestrálního projektu a jeho rámce – Adventura
Dva režimy zpracování příkazů Nyní bychom měli začít přemýšlet, jak definovat její tělo. Když se nad touto problematikou na chvíli zamyslíte, brzy si uvědomíte, že program pracuje ve dvou režimech: jinak se chová, když hra neběží, a jinak, když hra běží.
Když hra neběží, tak očekává, že bude zadán prázdný příkaz k odstartování hry. Jakékoliv jiné zadání považuje za špatné.
Když hra běží, očekává, že bude zadán jeden z definovaných příkazů. Zadání libovolného jiného příkazu včetně prázdného je opět špatně. Tomu by měla odpovídat i definice metody zpracovávající zadaný příkaz. Metoda by se tedy měla rozhodnout o způsobu zpracování zadaného příkazu podle toho, jestli hra běží. Metodu, zjišťující zda hra právě běží, jsme již definovali ve třídě hry. Je to metoda isAlive().
Metoda isAlive() ve třídě hry Jistě odhadnete, že ten, kdo se první dozví o tom, že se hra rozběhla, resp. že hra skončila, je správce příkazů. Nebylo by asi moudré, kdyby měl vždy předávat informaci o zahájení či ukončení hry instanci hry, které by se pak na počátku metody execute(String) na tento stav ptal. Daleko výhodnější bude, když si bude správce pamatovat vše sám, a pokud náhodou hru někdo o tuto informaci požádá, hra deleguje požadavek na svého správce příkazů. Upravíme proto definici metody ve třídě hry do podoby ve výpisu 5.5. Výpis 5.5: 14 15 16 17 18
Upravená definice metody isAlive() ve třídě OfficialApartmentGame
@Override public boolean isAlive() { return ACommand.isAlive(); }
Metoda isAlive() ve správci příkazů Nyní budeme postupovat obdobně, jako jsme před chvílí postupovali s metodou execute(String): zkopírujeme ji do třídy správce scénářů a doplníme modifikátor static. Jediným rozdílem bude, že ji umístíme do sekce přístupových metod objektu třídy, tj. do sekce class getters and setters. Tato metoda by měla zveřejňovat informaci, kterou je třeba si někde pamatovat. Při každém startu hry uložíme do dané proměnné true (hra běží) a po ukon-
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 86 z 240
Kapitola 5: Začínáme tvořit vlastní hru
87
čení hry do ní uložíme false. Přitom bude jedno, zda hra skončila přirozeně nebo vynuceně. Využijeme toho, že v Javě se atribut může jmenovat stejně jako metoda, protože to, že voláme metodu, překladač snadno pozná podle závorek za jejím názvem. Definujeme proto statickou proměnnou isAlive kterou můžeme hned v deklaraci inicializovat hodnotou false. Tutéž hodnotu sice dostane přiřazenu už při zavádění objektu třídy, ale takto zdůrazníme, že je to ta pravá počáteční hodnota pro danou aplikaci. Definice metody je soukromou záležitostí aplikace, takže metoda by neměla být definována jako veřejná, ale jako soukromá v balíčku (package private). Deklarace proměnné i definice metody jsou triviální, ale pokud si je chcete prohlédnout, najdete je ve výpisu 5.6 na straně 90.
Tady bych chtěl ještě jednou upozornit na „oblíbenou“ chybu, kdy voláte bezparametrickou metodu, zapomenete napsat prázdné závorky a strašně se divíte, že překladač tvrdí, že takovou proměnnou nezná.
Definice metody execute(String) Nyní se můžeme konečně definovat metodu execute(String). Její definice bude jednoduchá:
Pokud se hra ještě nehraje, začne řešit start hry. Aby se nám kód metody příliš nenatahoval, pověříme tím samostatnou metodu, které předáme zadaný příkaz. Metoda má na starosti odstartování hry, takže ji nazveme startGame.
Pokud se hra již hraje, jedná se pravděpodobně o zadání dalšího příkazu. Jeho provedením opět pověříme samostatnou metodu. Metoda bude mít na starosti zpracování běžného příkazu, takže ji nazveme executeCommonComand. Před vlastní rozhodování bychom mohli ještě doplnit příkaz, který ze zadaného příkazu odstraní případné počáteční a koncové mezery. Tato znaky jsou pro uživatele do jisté míry neviditelné a mohl by je proto do zadávaného příkazu vložit omylem. Bylo by škoda, kdyby kvůli těmto omylem vloženým znakům hra začala tvrdit, že uživatel zadal neznámý příkaz. Tuto čistící akci není moudré odkládat, protože pak bychom ji museli provést v obou právě volaných metodách. Příkaz pro odstranění případných počátečních a koncových mezer proto zadáme ještě před tím, než se začneme rozhodovat, zda se hra bude startovat, anebo zda se má provést řadový příkaz. Definici takto definované metody execute(String) najdete ve výpisu 5.6 na straně 90. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 87 z 240
88
Návrh semestrálního projektu a jeho rámce – Adventura
5.7
Metoda pro spuštění hry – startGame(String)
Metoda pro spuštění hry musí nejprve zjistit, jestli byl zadán správný startovací příkaz. Při definici metody execute(String) jsme již ze zadávaného příkazu odstranili případné počáteční a závěrečné mezery, čímž jsme uživateli odpustili případné zapomenuté mezery. Nyní by už měl být korektním zadávaným příkazem pouze prázdný řetězec.
Bude-li tedy příkazem opravdu prázdný řetězec, můžeme korektně odstartovat hru a vrátit řetězec se standardním uvítáním. Tento textový řetězec převezmeme z definice startovního kroku ve správci příkazů.
Nebude-li
zadaným příkazem prázdný řetězec, pošleme uživateli zprávu, která jej upozorní, že hru lze odstartovat pouze prázdným příkazem. Podobu této zprávy definuje počáteční krok chybového scénáře, který právě tuto situaci testuje.
Definici takto definované metody startGame(String) najdete ve výpisu 5.6 na straně 90.
5.8
Metoda pro pracování běžného příkazu – executeCommonComand(String)
Zpracování běžného příkazu bude trochu složitější, protože nejprve musíme v zadaném textu najít název zadávaného příkazu. To učiníme nejsnáze tak, že rozdělíme zadávaný text na slova. První slovo bude název zadávaného příkazu, další slova budou jeho parametry.
Rozdělení příkazového řádku na slova K tomuto účelu lze využít metody split(String), kterou mají ve svém portfoliu instance třídy String. Parametrem metody je regulární výraz specifikující text, který odděluje části textu. Metoda vrací pole textových řetězců („stringů“) oddělených textem zadaným v parametru. Musíme si nejprve ujasnit, jakou podobu by měl mít onen regulární výraz. Pokud by byla jednotlivá slova příkazu oddělena vždy jednou mezerou, stačilo by jako regulární výraz zadat mezeru. Jakmile by však bylo mezi dvěma slovy příkazu více mezer, metoda by nám v poli vrátila i prázdné řetězce představující text mezi těmito mezerami (protože mezi nimi není nic, vrátí prázdný řetězec).
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 88 z 240
89
Kapitola 5: Začínáme tvořit vlastní hru
Budeme-li však chtít být na uživatele hodní a velkoryse mu odpustit, že tu a tam oddělil jednotlivá slova více než jednou mezerou, musíme jako regulární výraz zadat text ".+", anebo ještě lépe text "\\s+". Nazveme-li parametr představující analyzovaný příkazový řádek commandLine a rozhodneme-li se uložit výsledek do lokální proměnné nazvané words, bude mít příkaz podobu: String[] words = commandLinde.split("\\s+"); Měli bychom ale myslet na to, že uživatel může příkaz zadávat velkými i malými písmeny. Když už jsme se jednou rozhodli, že budeme vše převádět na malá písmena (viz pasáž Úprava definice konstruktoru na straně 83), měli bychom tak učinit i nyní, aby nám pak velikost písmen nedělala problémy při porovnávaní textů. Upravíme proto příkaz do podoby: String[] words = commandLine.toLowerCase().split("\\s+");
Převedení počátečního slova na příkaz Příkazový řádek máme tedy rozdělen. Zbývá tedy určit příkaz specifikovaný prvním (pardon – nultým) slovem. K tomuto účelu jsme ale v pasáži Úprava definice konstruktoru na straně 83 zavedli statický atribut NAME_2_COMMAND (viz výpis 5.3 na straně 84) s mapou pro převod názvů příkazů na objekty příkazů. Zadáme-li této mapě název příkazu, mapa nám vrátí objekt, který je schopen daný příkaz zpracovat. Pokud mapa zná zadaný název a vrátí příslušný příkaz, máme vyhráno. Zavoláme jeho metodu execute(String...) a ta nám vrátí odpověď, kterou předáme volající metodě. Pokud mapa zadaný název příkazu nezná, vrátí null. V takovém případě musíme oznámit uživateli, že zadal neznámý příkaz. Podíváme se proto do správce scénářů, najdeme mezi kroky chybového scénáře krok testující reakci na neznámý příkaz, zjistíme, jak má vypadat text reakce na neznámý příkaz a příslušný text vrátíme jako funkční hodnotu. Definici takto definované metody executeCommonComand(String) najdete ve výpisu 5.6 na straně 90.
5.9
Prozatímní podoba definice třídy ACommand
První verzi třída ACommand tedy máme definovanou. Podobu zdrojového kódu, k níž jsme dospěli, si můžete prohlédnout ve výpisu 5.6.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 89 z 240
90
Návrh semestrálního projektu a jeho rámce – Adventura
Ve výpisu je oddělena část zabývající se definicemi vlastností a schopností objektu třídy (řádky 19 až 119) od části zabývající se definicí zabývající se definicemi vlastností a schopností vytvářených instancí, přesněji rodičovského podobjektu vytvářených instancí (řádky 122 až 205). Připomínám, že objekt třídy slouží jako správce instancí dceřiných tříd, přičemž tyto instance budou představovat jednotlivé příkazy vaší hry, tj. objekty, které budou umět reagovat na příslušné zadávané příkazy. Výpis 5.6: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
Prozatímní podoba definice třídy ACommand
/******************************************************************************* * Třída {@code EmptyACommand} je společným rodičem všech tříd, jejichž instance * mají na starosti interpretaci příkazů zadávaných uživatelem hrajícím hru. * Název spouštěného příkazu bývá většinou první slovo řádku zadávaného * z klávesnice a další slova pak bývají interpretována jako parametry. *
* Můžete ale definovat příkaz, který odstartuje konverzaci * (např. s osobou přítomnou v místnosti) a tím přepne systém do režimu, * v němž se zadávané texty neinterpretují jako příkazy, * ale předávají se definovanému objektu až do chvíle, * kdy uživatel rozhovor ukončí a objekt rozhovoru přepne hru zpět * do režimu klasických příkazů. * * @author Rudolf PECINOVSKÝ * @version 14.00.4526 */ abstract class ACommand implements ICommand { //== CONSTANT CLASS ATTRIBUTES ================================================= /** Mapa sdružující názvy příkazů s příslušnými objekty. */ private static final Map<String, ACommand> NAME_2_COMMAND = new HashMap<>();
//== VARIABLE CLASS ATTRIBUTES ================================================= /** Uchovává informaci o tom, zda se hra právě hraje * nebo jen čeká na spuštění. */ private static boolean isAlive = false;
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= /*************************************************************************** * Vrátí informaci o tom, je-li hra aktuálně spuštěná. * Spuštěnou hru není možno pustit znovu.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 90 z 240
Kapitola 5: Začínáme tvořit vlastní hru
91
41 * Chceme-li hru spustit znovu, musíme ji nejprve ukončit. 42 * 43 * @return Je-li hra spuštěná, vrátí {@code true}, 44 * jinak vrátí {@code false} 45 */ 46 static boolean isAlive() 47 { 48 return isAlive; 49 } 50 51 52 53 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 54 55 /*************************************************************************** 56 * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. 57 * 58 * @param command Zadávaný příkaz 59 * @return Textová odpověď hry na zadaný příkaz 60 */ 61 static String executeCommand(String command) 62 { 63 command = command.trim(); 64 String answer; 65 if (isAlive) { 66 answer = executeCommonComand(command); 67 } 68 else { 69 answer = startGame(command); 70 } 71 return answer; 72 } 73 74 75 76 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= 77 78 /*************************************************************************** 79 * Zjistí, jaký příkaz je zadáván, a jedná-li se o známý příkaz, 80 * provede jej. 81 * 82 * @param commandLine Zadávaný příkaz 83 * @return Odpověď hry na zadaný příkaz 84 */ 85 private static String executeCommonComand(String commandLine) 86 { 87 String[] words = commandLine.toLowerCase().split("\\s+"); 88 String commandName = words[0]; 89 ACommand command = NAME_2_COMMAND.get(commandName); 90 String answer; 91 if (command == null) { 92 answer = zNEZNÁMÝ_PŘÍKAZ; 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 91 z 240
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
Návrh semestrálního projektu a jeho rámce – Adventura
}
} else { answer = command.execute(words); } return answer;
/*************************************************************************** * Ověří, jestli je hra spouštěna správným (= prázdným) příkazem, * a pokud ano, spustí hru. * * @param command Příkaz spouštějící hru * @return Odpověď hry na zadaný příkaz */ private static String startGame(String command) { String answer; if (command.isEmpty()) { answer = zCELÉ_UVÍTÁNÍ; } else { answer = zNENÍ_START; } return answer; }
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Název daného příkazu. */ private final String name; /** Stručný popis daného příkazu. */ private final String description;
//== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** * Vytvoří rodičovský podobjekt vytvářeného příkazu hry. * * @param name Název vytvářeného příkazu * @param description Stručný popis vytvářeného příkazu
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 92 z 240
Kapitola 5: Začínáme tvořit vlastní hru
93
145 */ 146 ACommand(String name, String description) 147 { 148 this.name = name; 149 this.description = description; 150 ACommand put = NAME_2_COMMAND.put(name.toLowerCase(), this); 151 if (put != null) { 152 throw new IllegalArgumentException( 153 "\nPříkaz s názvem «" + name + "» byl již vytvořen"); 154 } 155 } 156 157 158 159 //== ABSTRACT METHODS ========================================================== 160 161 /*************************************************************************** 162 * Metoda realizující reakci hry na zadání daného příkazu. 163 * Počet parametrů je závislý na konkrétním příkazu, 164 * např. příkazy konec a nápověda nemají parametry, 165 * příkazy jdi a seber mají jeden parametr 166 * příkaz použij muže mít dva parametry atd. 167 * 168 * @param arguments Parametry příkazu; 169 * jejich počet muže byt pro každý příkaz jiný 170 * @return Text zprávy vypsané po provedeni příkazu 171 */ 172 @Override 173 abstract 174 public String execute(String... arguments) 175 ; 176 177 178 179 //== INSTANCE GETTERS AND SETTERS ============================================== 180 181 /*************************************************************************** 182 * Vrátí název příkazu, tj. text, který musí hráč zadat 183 * pro vyvolaní daného příkazu. 184 * 185 * @return Název příkazu 186 */ 187 @Override 188 public String getName() 189 { 190 return name; 191 } 192 193 194 /*************************************************************************** 195 * Vrátí popis příkazu s vysvětlením jeho funkce 196 * a významu jednotlivých parametru. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 93 z 240
94 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
Návrh semestrálního projektu a jeho rámce – Adventura * * @return Popis příkazu */ @Override public String getDescription() { return description; }
//== OTHER NON-PRIVATE INSTANCE METHODS ======================================== //== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= }
5.10 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 3.8 Co jsme prozatím naprogramovali na straně 64, jsme přidali následující:
Definovali
jsme základ třídy hry – třídu OfficialApartmentGame. Některé její metody sice prozatím pouze vyhazují výjimku UnsupportedOperationException, takže překladač je přeloží, ale metody ještě nesplňují požadovaný kontrakt, ale počítáme s tím, že je časem doplníme.
Definovali jsme třídu ACommand, jejíž instance (přesněji instance jejích potomků) budou představovat jednotlivé příkazy a objekt třídy pak bude představovat správce těchto příkazů. Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 5.1.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 94 z 240
95
Kapitola 5: Začínáme tvořit vlastní hru
Obrázek 5.1 Aktuální diagram tříd projektu poté, co byla definována kostra třídy a správce příkazů
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A05z_AppartmentGame.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 95 z 240
96 6.
Návrh semestrálního projektu a jeho rámce – Adventura Vytváříme svět hry
Kapitola 6 Vytváříme svět hry
6.1
Co se v kapitole naučíme V této kapitole budeme pokračovat s vývojem hry. Stále budeme používat osvědčený postup, při němž vždy spustíme testy a pak se budeme snažit odstranit chybu, na níž nás testy upozorní. To nás postupně dovede k tomu, že vytvoříme celý svět naší hry.
Analýza chybového hlášení
Při spuštění testu programu dovedeného do stavu z konce předchozí kapitoly nám testovací program vypíše: Ukončení bylo způsobeno vyhozením výjimky: java.lang.UnsupportedOperationException: Metoda ještě není hotova. at org.edupgm.adventure.common.rup14p.OfficialApartmentGame.getWorld(OfficialApartmentGame.java:175) at cz.pecinovsky.adv_framework.test_util.gamet.GameStepTest.verify(GameStepTest.java:69) atd. Podíváme-li se na odkazované místo, zjistíme, že metoda getWorld má stále ještě prozatímní tělo vyhazující výjimku UnsupportedOperationException. Nahradíme příkaz k vyhození výjimky slovem return, čímž do programu zaneseme syntaktickou chybu, Pojďme se tedy podívat na to, jak bychom takový svět hry naprogramovali.
Svět hry jako správce prostor hry Než se pustíme do vytváření třídy světa hry, měli bychom si v pasážích Svět – IWorld na straně 67 a IWorld na straně 71 připomenout, jaký má být účel tohoto 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 96 z 240
Kapitola 6: Vytváříme svět hry
97
objektu v našem programu. Tehdy jsme si řekli, že instance této třídy by měla být jedináček, který bude sloužit jako správce prostorů světa hry. Bude nám umět prozradit, jaké prostory ve hře vystupují a ve kterém z nich se právě nachází hráč. Takže už to víme a můžeme jít tvořit-
6.2
Vytvoření třídy světa hry – Apartment
Začátek vytváření třídy světa hry bude obdobný jako při definice tříd správce scénářů, třídy vlastní hry a třídy správce příkazů: ve zdrojových kódech rámce (frameworku) navštívíme balíček empty_classes a zkopírujeme z něj třídu EmptyWorld obsahující předpřipravenou kostru vytvářené třídy světa hry. Zkopírovanou třídu hned přejmenujeme tak, aby odpovídala duchu naší hry. Když je světem naší hry služební byt, měli bychom tuto třídu pojmenovat OfficialApartment. Pro zkrácení ale vypustíme informaci o tom, že byt je služební, a třídu nazveme jednoduše Apartment. Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Kostra třídy již počítá s tím, že instance reprezentující svět hry bude definována jako jedináček, a proto má již definovaný statický atribut SINGLETON uchovávají odkaz na tohoto jedináčka, tovární metodu getInstance() vracející tento odkaz a soukromý konstruktor, který je prozatím prázdný. Třída současně deklaruje, že implementuje interfejs IWorld a definuje proto kostry jím deklarovaných metod. Jejich těla bychom měli nyní definovat, protože je zřejmé, že je bude testovací program volat (k čemu by jinak instanci světa chtě, že).
Definice metody getAllAreas() Jako první je ve zdrojovém kódu uvedena metoda getAllAreas(), která má vrátit kolekci všech prostorů hry. Těžko ale můžeme vytvářet kolekci prostorů, když ještě nemáme definovánu třídu prostorů. Musíme proto nejprve definovat tu.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 97 z 240
98
Návrh semestrálního projektu a jeho rámce – Adventura
6.3
Třída prostorů hry – Room
Opět začneme tím, že se nejprve podíváme na pasáže Prostor – ISpace na straně 67 a ISpace na straně 71, abychom si připomněli, jaké vlastnosti jsme prostorům naplánovali, když jsme probírali návrh rámce hry. Zde se dozvíme, že každý prostor musí mít své jméno, musí znát své aktuální sousedy a musí vědět, jaké se v něm právě nacházejí objekty. Třídu prostorů začneme vytvářet standardním způsobem: zkopírujeme z balíčku empty_classes její vzor – třídu EmptyArea. Při té příležitosti ji hned přejmenujeme. Prostory hry, kterou právě vytváříme, jsou místnosti. Proto se mateřská třída těchto prostorů bude jmenovat Room. Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Opět si můžete všimnout, že třída není veřejná, protože je určená pouze pro vnitřní potřebu hry. Budou-li její instance komunikovat s okolím, tak pouze jako instance interfejsu IArea. V kostře třídy jsou předpřipravené implementace tří abstraktních metod definovaných v implementovaném interfejsu. Tyto metody mají za úkol poskytnout na požádání název prostoru (v naší hře místnosti), kolekci jeho aktuálních sousedů a kolekci objektů, které se v něm právě nacházejí. Abychom instance mohla tyto informace předat, musí je nejprve znát. Tyto informace jednoznačně charakterizují daný prostor a jeho aktuální stav. Nejlepší místo, kde můžeme každému prostou předat informace o jeho výchozím stavu, jsou parametry jeho konstruktoru. Pojďme tedy nejprve definovat konstruktor.
Konstruktor místnosti Konstruktor našich místností by tedy měl mít tři parametry, v nichž bychom mu předali název místnosti, její počáteční sousedy a počáteční objekty. Bohužel, to se nám nemůže podařit. Není problém prozradit vytvářené místnosti její název a nemělo by být ani těžké dodat počáteční objekty. Problémem ale jsou sousedé dané místnosti. Představte si, že vytváříme první místnost, která má sousedit s druhou a třetí. Potřebovali bychom proto předat konstruktoru první místnosti odkaz na druhou a třetí místnost. Tyto místnosti ale ještě neexistují (vytváříme teprve tu první), takže nemá smysl uvažovat o nějakém odkazu. My ale můžeme tento problém obejít tak, že konstruktoru místností nepředáme odkazy na sousední místnosti, ale pouze názvy těchto místností. Budeme pak 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 98 z 240
99
Kapitola 6: Vytváříme svět hry
počítat s tím, že budeme umět místnost v pravý okamžik požádat, aby tyto názvy převedla na příslušné objekty. Pokud se budeme chtít obdobně zachovat i k objektům, mohla by hlavička konstruktoru být ve tvaru: Room(String name, String[] neighborNames, String... objectNames) Možná některé z vás udiví, proč předávám názvy sousedů jinak než názvy objektů. Je to proto, že uvedu-li za názvem typu tří tečky, stanou se všechny následující parametry prvky jednorozměrného pole, avšak já nemusím příslušné pole deklarovat, ale mohu pouze vyjmenovat jeho prvky a o definici potřebného pole se postará překladač. Bohužel, takto mohu zadat pouze poslední parametr, takže předchozí parametr musím explicitně deklarovat jako jednorozměrné pole. se někteří další budou ptát, proč zde používám pole, když jsem již Možná několikrát řekl, že pole se postupně přestávají používat a jsou nahrazována kolekcemi. To je sice pravda, ale stále existuje řada situací, kdy je práce s poli jednodušší, aniž by přitom hrozilo nebezpečí zneužití jejich neschopnosti ochránit uložená data a absence některých dalších vlastností. Jednou z takových situací je předání skupiny dat metodě.
Vraťme se k našemu konstruktoru. Jeho parametry jsme vymysleli, ale na konstruktoru nyní je, aby si je zapamatoval. Pro každou hodnotu, kterou si potřebujeme zapamatovat, musíme definovat atribut, do nějž ji uložíme. Všechny tři potřebné atributy přitom můžeme definovat jako konstanty, jejichž hodnota se nebude měnit. Deklarujeme tedy konstruktor a první atributy podle výpisu 6.1. Výpis 6.1:
Definice konstruktoru instancí třídy Room a jím inicializovaných atributů
1 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 2 3 /** Název dané místnosti. */ 4 private final String name; 5 6 /** Názvy sousedů místnosti na počátku hry. */ 7 private final String[] neighborNames; 8 9 /** Názvy objektů v místnosti na počátku hry. */ 10 private final String[] objectNames; 11 12 13 //== CONSTUCTORS AND FACTORY METHODS =========================================== 14 15 /*************************************************************************** 16 * Vytvoří novou místnost se zadaným názvem a 17 * zadanými názvy jejich počátečních sousedů a objektů. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 99 z 240
100 18 19 20 21 22 23 24 25 26 27 28
Návrh semestrálního projektu a jeho rámce – Adventura * * @param name Název dané místnosti * @param neighborNames Názvy sousedů místnosti na počátku hry * @param objectNames Názvy objektů v místnosti na počátku hry */ Room(String name, String[] neighborNames, String... objectNames) { this.name = name; this.neighborNames = neighborNames; this.objectNames = objectNames; }
Metoda getName() Definice metody getName() je tak jednoduchá, že ji tu neuvádím a pasáž o ní je tu pouze pro úplnost.
Metoda getNeighbors() Definice metody getNeighbors() bude nepatrně složitější. První věc, kterou musím vyřešit, je získání kolekce, kterou mám vrátit. Jediné, co mám zatím k dispozici, je pole názvů počátečních sousedů. Objekt ale potřebuje kolekci aktuálních sousedů, ne jen jejich názvů. Tu musí objekt za prvé někde vyrobit a za druhé si ji musí průběžně pamatovat. Jak už jsem mnohokrát řekl: máme-li si něco pamatovat, potřebujeme atribut. Pro zapamatování si aktuálních sousedů proto definujeme kolekci: Collection
Strana 100 z 240
101
Kapitola 6: Vytváříme svět hry
obsahu je chod hry životně závislý. Existují dva způsoby, jak se s tímto problémem vypořádat:
Místo originální kolekce vrátíme její kopii. Vrátíme kolekci zabalenou do objektu, který nedovolí změnit její obsah. Protože druhá metoda je efektivnější, zvolíme ji. My už jsme ji jednou použili, i když jsem vám o tom tehdy neřekl. Pokud si vzpomenete na výpis 2.3 na straně 30, tak tam je na řádcích 114 a 115 podobný problém řešen: využije se toho, že knihovní třída java.util.Collections definuje sadu metod, které zabalí kolekci převzatou v parametru do jiné kolekce, která nedovolí obsah obalené kolekce změnit. Tělo metody getNeighbors() by pak mohlo být tvořeno příkazem: return Collections.unmodifiableCollection(neighbors);
Metoda getObjects() Obdobně bychom to mohli udělat i s metodou getObjects(). Problémem ale je, že tato metoda má vracet kolekci objektů v daném prostoru, a my jsme ještě mateřskou třídu těchto objektů nedefinovali. Musíme to nejprve napravit.
6.4
Třída objektů v prostorech – Thing
Začátek definice bude standardní: v pasážích Objekt – IObject na straně 68 a IObject na straně 71 si nejprve připomeneme, jaké vlastnosti a schopnosti objektů jsme naplánovali. Zde se dozvíme, že objekt musí znát svůj název a svoji váhu. Pak z balíčku empty_classes v rámci zkopírujeme třídu EmptyThing. Možná někoho z vás zarazí, že se třída nejmenuje EmptyObject, ale nechtěl jsem riskovat, že někdo při kopírování pouze odmaže slova Empty a vytvoří tak třídu se stejným názvem, jako má společný prapředek všech objektových datových typů. To by mohlo na řadě míst vést ke zmatení pojmů. Takže zkopírujeme třídu EmptyThing a přejmenujeme ji na Thing. Má-li objekt znát svůj název a váhu, měli bychom obě informaci předat jeho konstruktoru v parametru. Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*;
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 101 z 240
102
Návrh semestrálního projektu a jeho rámce – Adventura
Váha, přenositelnost Oproti minulým třídám ale uděláme jednu změnu. Abych vám ji vysvětlil, nejprve odbočím. Přiznejme si, že váha objektu není primární údaj. Podle zadání stačí, když se všechny objekty rozdělí na přenositelné a nepřenositelné. Váhu jsme do rámce přidali jenom proto, abychom poněkud rozšířili možnosti těm, kteří chtějí vymyslet něco rafinovanějšího. Přiznejme si ale, že většina her vystačí s binární informací přenositelný × nepřenositelný. Pro takovouto jednoduchou informaci nepotřebujeme zvláštní parametr. Můžeme ji předávat i tak, že ji bude charakterizovat počáteční znak zadaného názvu objektu a teprve další znaky budou definovat jeho skutečný název. Tím se trochu zjednoduší volání konstruktorů objektů – všem bude stačit jenom jeden parametr, kterým bude název objektu doplněný na počátku znakem charakterizujícím jeho přenositelnost. Pokud byste dělili objekty na více kategorií, mohl by tento počáteční znak mít i další významy. V ukázkové hře, kterou zde spolu definujeme, rozlišuje lednička nápoje na alkoholické a nealkoholické. Protože se o alkoholických nápojích ví, že jsou přenositelné, mohli bychom definovat tři interpretace počátečního znaku:
Bude-li počátečním znakem znak '#', bude se jednat o nepřenositelný objekt. Bude-li počátečním znakem znak '@', bude se jednat o alkoholický nápoj (ten je přenositelný).
Abych to ještě vylepšil, přidám znak '2', který bude označovat, že daný objekt je třeba vzít do obou rukou, a že má tedy váhu 2.
Bude-li počátečním znakem cokoliv jiného, bude se jednat běžný přenositelný objekt, který není alkoholickým nápojem ani objektem, k jehož zvednutí potřebuji obě ruce. Poslední případ by bylo vhodné zpřísnit, aby i pro obecný přenositelný objekt byl definován nějaký konkrétní znak. Za prvé se tak předejde chybám z překlepů, při nichž někdo zapomene před název objektu ten první znak přidat, a za druhé budeme mít otevřený prostor pro případné přidání dalších druhů objektů. Přidáme-li do definice třídy i zpracování informace o alkoholických nápojích, budeme muset definovat tři atributy – v jednom si budeme pamatovat název objektu, v druhém jeho váhu a ve třetím to, jedná-li se o alkoholický nápoj. Ke každému atributu pak bude definována zjišťovací metoda („getr“), která bude na požádání vracet jeho hodnotu. Když si u objektů pamatujeme pouze to, zda jsou či nejsou přenositelné, tak bychom si měli ještě ujasnit, jakou budou vracet váhu. V našem případě je to jednoduché: přenositelné objekty budou mít váhu 1, nepřenositelné musí mít větší váhu, než bude kapacita batohu. Nejlepší bude, když tuto váhu definujeme pro49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 102 z 240
Kapitola 6: Vytváříme svět hry
103
střednictvím statické konstanty nazvané např. HEAVY, které přiřadíme dostatečně velkou hodnotu – např. největší přípustné celé číslo.
Zveřejnění příznaků Při definici příznaků bychom si měli uvědomit, že se budou používat na více místech: za prvé zde při vytváření objektu a za druhé v místě, kde se bude daný objekt definovat. Není vhodné, aby si programátor musel pamatovat, jaký znak je pro ten který příznak použit a už vůbec není vhodné, aby se tyto znaky objevovaly někde roztroušené v programu. Kdybychom je náhodou museli někdy později měnit (např. proto, že by se některý z nich měl stát součástí názvu nově přidaného objektu), museli bychom najít všechny jejich výskyty v programu a všude je správně změnit. Jak jistě tušíte, správně řešení je definovat tyto znaky jako nesoukromé statické konstanty, které budou ostatní programy importovat. Nemusíme je deklarovat jako veřejné, protože nám stačí, když budou dostupné pouze ostatním třídám hry, a ty jsou ve stejném balíčku.
Vlastní definice Po všech těchto úvahách by měla být definice třídy objektů jasná. Ti, kteří mají ještě nějaké nejasnosti, si ji mohou prohlédnout ve výpisu 6.2. Výpis 6.2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Definice třídy Thing, jejíž instance představují objekty
/******************************************************************************* * Instance třídy {@code Thing} přestavují objekty v místnostech. * Objekty mohou být jak věci, tak i osoby či jiné skutečnosti (vůně, * světlo, fluidum, ...). */ class Thing implements IObject { //== CONSTANT CLASS ATTRIBUTES ================================================= /** Příznak nepřenositelnosti objektu. */ static final char H = '#'; /** Příznak alkoholického nápoje. */ static final char A = '@'; /** Příznak nutnosti použít ke zvednutí objektu obě ruce. */ static final char D = '2'; /** Příznak standardního přenositelného objektu. */ static final char S = '_';
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 103 z 240
104 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
Návrh semestrálního projektu a jeho rámce – Adventura /** Váha nepřenositelného objektu. */ private static final int HEAVY = Integer.MAX_VALUE;
//== VARIABLE CLASS ATTRIBUTES =================================================
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= //== OTHER NON-PRIVATE CLASS METHODS =========================================== //== PRIVATE AND AUXILIARY CLASS METHODS =======================================
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Název objektu. */ private final String name; /** Váha objektu. */ private final int weight; /** Alkoholičnost objektu. */ private final boolean isAlcoholic; //== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** * Vytvoří objekt se zadaným názvem, váhou a alkoholičností. * Váha a alkoholičnost objektu se zadává v počátečním znaku parametru. * * Je-li počátečním znakem znak {@code '#'}, * jedná se o nepřenositelný objekt. * Je-li počátečním znakem znak {@code '@'}, * jedná se o přenositelný objekt, alkoholický nápoj. * Je-li počátečním znakem znak {@code '_'}, * jedná se o běžný přenositelný objekt. *
* Jiné počáteční znaky jsou zakázány. * * @param superName Supernázev, jehož počátečním znakem je informace
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 104 z 240
Kapitola 6: Vytváříme svět hry
105
73 * o váze a alkoholičnosti objektu a zbylé znaky 74 * představují vlastní název objektu 75 */ 76 Thing(String superName) 77 { 78 boolean alcoholic = false; 79 switch(superName.charAt(0)) 80 { 81 case H: 82 weight = HEAVY; 83 break; 84 85 case D: 86 weight = 2; 87 break; 88 89 case A: 90 alcoholic = true; 91 92 case S: 93 weight = 1; 94 break; 95 96 default: 97 throw new IllegalArgumentException( 98 "\nNepovolený počáteční znak supernázvu"); 99 } 100 name = superName.substring(1); 101 isAlcoholic = alcoholic; 102 } 103 104 105 106 //== ABSTRACT METHODS ========================================================== 107 //== INSTANCE GETTERS AND SETTERS ============================================== 108 109 /*************************************************************************** 110 * Vrátí název prostoru. 111 * 112 * @return Název prostoru 113 */ 114 @Override 115 public String getName() 116 { 117 return name; 118 } 119 120 121 /*************************************************************************** 122 * Vrátí váhu objektu, resp. charakteristiku jí odpovídající. 123 * Objekty, které není možno zvednout, vrací -1. 124 * 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 105 z 240
106 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
Návrh semestrálního projektu a jeho rámce – Adventura * @return * */ @Override public int { return }
Váha objektu nebo hodnota -1 charakterizující, že daný objekt není možno zvednou a umístit do batohu. getWeight() weight;
/*************************************************************************** * Vrátí informaci o alkoholičnosti objektu. * * @return Jedná-li se o alkoholický nápoj, vrátí {@code true}, * v opačném případě vrátí {@code false} */ boolean isAlcoholic() { return isAlcoholic; }
//== OTHER NON-PRIVATE INSTANCE METHODS ======================================== //== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= }
6.5
Dokončení definice třídy Room
Třídu objektů v místnostech máme definovanou, můžeme se tedy vrátit do třídy Room a definovat pro místnost kolekci objects, která bude obsahovat objekty, jež se v ní nacházejí. Vzápětí definujeme i tělo metody getObjects(), která bude tuto kolekci vracet. Přesněji: bude vracet tuto kolekci zabalenou do objektu, který nedovolí změnit její obsah tak, jak jsme si to ukazovali v pasáži Metoda getNeighbors() na straně 100. Jak si jistě domyslíte, tělo metody getObjects() bude obdobou těla metody getNeighbors().
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 106 z 240
Kapitola 6: Vytváříme svět hry
6.6
107
Dokončení definice třídy Apartment
Dokončili jsme definici třídy Room a jejích povinných metod, takže se můžeme konečně vrátit k definici třídy Apartment, jejíž instance funguje jako správce všech místností v bytě.
Metoda getAllAreas() Ve třídě Apartment zůstalo nedokončené tělo metody getAllAreas(). Dostáváme se do situace, kterou jsme již dvakrát řešili ve třídě Room. Její řešení máme již nacvičené: 1. Definujeme kolekci hodnot, které si má objekt pamatovat – zde to bude ko-
lekce místností. Protože se kolekce nebude v průběhu hry měnit, můžeme ji definovat jako konstantu: private final Collection
kolekce, která nedovolí změnit její obsah: return Collections.unmodifiableCollection(rooms); 3. Změníme deklaraci hlavičky metody tak, aby z ní bylo, že metoda vrací ko-
lekci instancí třídy Room. Po této úpravě budou vlastnosti metody nadále odpovídat požadavku implementovaného interfejsu, protože ten žádá deklaraci metody, která vrací kolekci něčeho, co implementuje interfejs IArea, což naše místnosti jsou, takže překladač nebude nic namítat. Hlavička metody tedy bude mít tvar: public Collection
Doplnění konstruktoru Po těchto úpravách ale budou NetBeans hlásit chybu v konstruktoru, protože se jim nebude líbit, že jsme atribut rooms definovali jako konstantu, a přitom jsme mu nikde nepřiřadili jeho hodnotu. Konstantám se musí jejich hodnota přiřadit buď v deklaraci, anebo v konstruktoru. Poslední dobou sílí mezi programátory přesvědčení, že není vhodné inicializovat atributy v deklaraci, protože pak jsou inicializace rozesety na více místech zdrojového kódu a programátor o nich ztrácí přehled. Doplníme proto do konstruktoru inicializační příkaz pro vytvoření příslušné kolekce: rooms = new ArrayList<>(); 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 107 z 240
108
Návrh semestrálního projektu a jeho rámce – Adventura
Při té příležitosti bychom si mohli uvědomit, že kolekce je zatím prázdná a že by konstruktor bytu mohl být tím správným místem pro vytvoření jednotlivých místností. Doplníme tedy příkazy, které vytvoří jednotlivé místnosti a vloží je do právě vytvořené kolekce. Parametry konstruktorů vytvářených místností přitom přebíráme z definic testovacích kroků ve správci scénářů. První výskyt daného prostoru ve scénáři vždy obsahuje názvy výchozích sousedů a objektů v daném prostoru. Při definici prostorů nesmíme zapomenout ani na nestandardní prostory (truhly, batohy, desky stolu, …), o nichž jsem hovořil v podkapitole 1.1 Koncepce hry na straně 18 a mezi něž patří v naší hře lednice. Upravenou definici konstruktoru pro naši hru s procházením služebního bytu si můžete prohlédnout ve výpisu 6.3. Všimněte si, že názvy objektů předávané konstruktoru místností sčítám s prefixem (předponou) definujícím příznak specifikující typ objektu (hovořil jsem o něm v pasáži Váha, přenositelnost na straně 102). Výpis 6.3:
Upravená definice konstruktoru instancí třídy Apartment
1 private Apartment() 2 { 3 rooms = new ArrayList<>(); 4 rooms.add(new Room(PŘEDSÍŇ, 5 new String[] {LOŽNICE, OBÝVÁK, KOUPELNA}, 6 H+BOTNÍK, S+DEŠTNÍK)); 7 rooms.add(new Room(LOŽNICE, 8 new String[] {PŘEDSÍŇ}, 9 H+POSTEL, H+ZRCADLO, S+ŽUPAN)); 10 rooms.add(new Room(OBÝVÁK, 11 new String[] {PŘEDSÍŇ, KUCHYŇ}, 12 H+TELEVIZE)); 13 rooms.add(new Room(KOUPELNA, 14 new String[] {PŘEDSÍŇ}, 15 S+BRÝLE, H+UMYVADLO, S+ČASOPIS)); 16 rooms.add(new Room(KUCHYŇ, 17 new String[] {OBÝVÁK, LOŽNICE}, 18 H+LEDNIČKA, S+PAPÍR)); 19 rooms.add(new Room(LEDNIČKA, 20 new String[] {}, 21 A+PIVO, A+PIVO, A+PIVO, S+SALÁM, S+HOUSKA, 22 A+VÍNO, A+RUM)); 23 }
Metoda getCurrentArea() Přesuneme se na další metodu – getCurrentArea(). Ta má vrátit prostor, v němž se hráč právě nachází. Ten bychom si opět měli pamatovat. V sekci instančních proměnných proto definujeme atribut 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 108 z 240
109
Kapitola 6: Vytváříme svět hry private Room currentArea;
a do těla metody getCurrentArea() vložíme příkaz, který bude hodnotu tohoto atributu vracet. Současně upravíme typ její návratové hodnoty na Room. Opět se jedná pouze o upřesnění datového typu specifikovaného rodičem. Třída Room implementuje interfejs IArea, takže se její instance mohou vydávat za instance tohoto interfejsu a překladač nebude vůči našemu upřesnění nic namítat. Výsledná podoba metody by měla odpovídat výpisu Chyba! Nenalezen zdroj odkazů.. Výpis 6.4: 1 2 3 4 5
Upravená definice metody getCurrentArea() ve třídě Apartment
@Override Room getCurrentArea() { return currentArea; }
6.7
Test
Tak jsme doplnili vše, čeho jsme si všimli. Určitě jsme ještě řadu věcí přehlédli, ale na ty nás upozorní testy. Spustíme tedy test, který skončí stejně jako minule – viz podkapitolu 6.1 Analýza chybového hlášení na straně 96. Zapomněli jsme upravit definici metody getWorld() ve třídě hry. Nahradíme tělo vyhazující výjimku příkazem vracejícím odkaz na instanci bytu a současně upřesníme typ její návratové hodnoty a místo IWorld uvedeme přesnější Apartment. Výsledná podoba metody by měla odpovídat výpisu 6.5. Výpis 6.5: 1 2 3 4 5
Upravená definice metody getWorld()
@Override public Apartment getWorld() { return Apartment.getInstance(); }
6.8
Úprava informací o stavu hry
Když spustíme hru nyní, již se doopravdy spustí, vypíše úvodní zprávu, ale pak se opět zasekne a testovací program vypíše: Ukončení bylo způsobeno vyhozením výjimky: java.lang.IllegalStateException: Hra má běžet, a přitom tvrdí, že je ukončena 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 109 z 240
110
Návrh semestrálního projektu a jeho rámce – Adventura
Na to, jestli hra běží či neběží, se ptáme voláním metody isAlive() (viz pasáž IGame na straně 72).V pasáži Metoda isAlive() ve třídě hry na straně 86 jsme ale zodpovědnost za zprostředkování této informace delegovali na správce příkazů, kterým je objekt třídy ACommand. V něm jsme však vše definovali jenom provizorně s tím, že to opravíme, až na to testovací program přijde. Právě na to došlo. Při zpracování startovacího příkazu jsme např. nezměnili stav atributu isAlive na true. To bychom měli napravit. Do kódu, který spouští hru (v mém programu je to metoda startGame(String) ve třídě ACommand), proto přidáme příkaz isAlive = true; Podíváme-li se na poslední výstup programu, zjistíme, že je tam ještě jedno oznámení výjimky. Z něj se dozvíme, že: java.lang.UnsupportedOperationException: Metoda ještě není hotova. at org.edupgm.adventure.common.rup14p.OfficialApartmentGame.stop(OfficialApartmentGame.java:216) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.executeScenario(GameTRunTest.java:215) atc. Testovací program nás zde upozorňuje, že jsme ještě nedefinovali metodu stop() pro programové ukončení hry. Tak to hned napravíme. Použijeme obdobně řešení, jako jsme použili u metody isAlive() – ve třídě hry delegujeme zodpovědnost za správnou reakci na správce příkazů a v něm definujeme stejnojmennou metodu (jenom nemusí být veřejná), která nastaví atributu isAlive hodnotu false. Definice obou těchto dvou metod je natolik jednoduchá, že ji tu ani nebudu uvádět.
6.9
Úvahy o inicializaci prostorů
Při dalším spuštění testu se dozvíme: Při vyhodnocování odpovědi se objevil problém: ============================================================================= Svět hry nevrátil aktuální prostor Metodu getCurrentArea(), která má aktuální prostor vrátit, jsme sice v pasáži Metoda getCurrentArea() na straně 108 definovali, ale nijak jsme přitom dále nerozebírali, jak zařídíme, aby ten v atributu currentArea opravdu byla požadovaná informace. Aktuální prostor známe na počátku hry. Při té příležitosti jej můžeme nastavit. Jeho budoucími změnami pak pověříme příkazy, které jej budou měnit. Pojďme se tedy zamyslet nad optimální definicí počátečního nastavení.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 110 z 240
Kapitola 6: Vytváříme svět hry
111
Prostory jsme sice vyráběli v konstruktoru třídy Apartment. Tam ale nemůžeme počáteční prostor nastavovat, protože jej musíme nastavit při každém spuštění hry. Ono se na počátku hry musí nastavovat více věcí – správce prostorů např. musí zařídit, aby všechny prostory znaly své počáteční sousedy a objekty. Jako rozumné řešení se jeví definovat inicializační metodu, kterou by volal kód spouštějící hru. Protože inicializovat něco na počátku hry nebude určitě muset jenom správce prostorů, ale i jiné objekty, doporučoval bych následující řešení:
Ve
správci příkazů (třída ACommand) definujeme metodu initialize(), kterou budeme volat z místa, kde se startuje hra.
Do
této metody pak budeme přidávat volání stejnojmenných metod všech objektů, které budou potřebovat na počátku hry něco inicializovat.
Ve správci prostor definujeme stejnojmennou metodu, která nastaví prostor, v němž se hráč nachází na počátku hry. A když už jsme u té inicializace, oběhne všechny svěřené prostory a každý z nich požádá, aby se také inicializoval.
Prostory se inicializují tak, že vezmou svá pole s názvy počátečních sousedů, resp. počátečních objektů a na jejich základě vytvoří odpovídající kolekce příslušných objektů. Doporučuji, aby se pro inicializaci každého z nich definovala zvláštní metoda.
6.10 Realizace inicializace prostorů Teoreticky jsme to zvládli. Takže nyní ještě praxe.
Inicializace ve třídě ACommand Začneme u kořene, kterým je třída ACommand. V ní upravíme kód pro start hry, takže nyní bude mít podoby z výpisu 6.6. Výpis 6.6:
Upravená definice metody startGame(String) spolu s metodou initialize() ve třídě ACommand
1 private static String startGame(String command) 2 { 3 String answer; 4 if (command.isEmpty()) { 5 answer = zUVÍTÁNÍ; 6 initialize(); 7 isAlive = true; 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 111 z 240
112 8 9 10 11 12 13 14 15 16 17 18 19
Návrh semestrálního projektu a jeho rámce – Adventura
}
} else { answer = zNENÍ_START; } return answer;
static void initialize() { Apartment.getInstance().initialize(); }
Inicializace ve třídě Apartment Správce příkazů jsme tedy zvládli. Můžeme se přesunout od správce prostor, tj. do třídy Room. Jak už jsem řekl, zde by inicializační metody měla nastavit výchozí místnost a pak postupně oslovit všechny prostory a požádat je, aby se inicializovaly. Její kód by mohl vypadat např. podle výpisu 6.7. Výpis 6.7:
Definice metody initialize()ve třídě Apartment
1 void initialize() 2 { 3 currentArea = INamed.get(PŘEDSÍŇ, rooms).get(); 4 rooms.forEach(Room::initialize); 5 }
Inicializace instancí třídy Room Ve třídě Room bych doporučil definovat metodu initialize() jako metodu se dvěma příkazy: první inicializuje sousedy, druhý objekty. Tato trojice inicializačních metod by mohly vypadat podle výpisu 6.8. Výpis 6.8:
Definice metod initialize(), initializeNeighbors() a initializeObjects() ve třídě Room
1 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 2 3 /*************************************************************************** 4 * Nastaví výchozí stav dané místnosti na počátku hry. 5 */ 6 void initialize() 7 { 8 initializeNeihgbors(); 9 inicitalizeObjects(); 10 } 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 112 z 240
113
Kapitola 6: Vytváříme svět hry
11 12 13 14 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== 15 16 /*************************************************************************** 17 * Nastaví výchozí sousedy dané místnosti na počátku hry 18 * a zapamatuje si jejich kolekci v atributu {@link #neighbors}. 19 */ 20 private void initializeNeihgbors() 21 { 22 Apartment apartment = Apartment.getInstance(); 23 neighbors = Arrays.stream(neighborNames) 24 .map(apartment::getRoom) 25 .collect(Collectors.toList()); 26 } 27 28 29 /*************************************************************************** 30 * Nastaví výchozí objekty v dané místnosti na počátku hry 31 * a zapamatuje si jejich kolekci v atributu {@link #objects}. 32 */ 33 private void inicitalizeObjects() 34 { 35 objects = Arrays.stream(objectNames) 36 .map(Thing::new) 37 .collect(Collectors.toList()); 38 } Jak vidíte, metoda pro nastavení výchozí sady objektů v prostoru je maličko jednodušší. Nejprve vytvoří datovod názvů objektů, jeho prvky pak s využitím konstruktoru převede na objekty, které pak vloží do nově vytvořené kolekce a tu uloží do příslušného atributu. Metoda pro nastavení počátečních sousedů potřebuje vybírat sousedy z již existujících objektů. Proto si nejprve zapamatuje odkaz na správce prostor, který má přehled o všech prostorech hry. Pak vytvoří datovod názvů svých sousedů, tyto názvy s pomocí zapamatovaného správce prostorů převede na odpovídající místnosti – sousedy, které se následně uloží do nově vytvořené kolekce. Tu pak vloží do příslušného atributu. V popsané metodě jsme využili toho, že správce prostor definuje metodu getRoom(String), o níž jsme doposud nehovořili. Tuto metodu jsem pro něj definoval proto, že lze očekávat, že požadavek o získání prostoru se zadaným názvem se bude objevovat častěji. Výpis 6.9:
Definice metody getRoom(String) ve třídě Apartment
1 /*************************************************************************** 2 * Vrátí odkaz na místnost se zadaným názvem. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 113 z 240
114
Návrh semestrálního projektu a jeho rámce – Adventura
3 * 4 * @param name Název požadované místnosti 5 * @return Místnost se zadaným názvem (nezávisle na velkosti písmen) 6 * nebo {@code null} v případě, že taková místnost neexistuje 7 */ 8 Room getRoom(String name) 9 { 10 Optional
6.11 Test Test spuštěný po všech výše uvedených úpravách sice opět vyhodí výjimku, ale bude vidět, že jsme se dostali zase o kus dál. Tentokrát se dozvíme:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 114 z 240
Kapitola 6: Vytváříme svět hry
115
Vítáme vás ve služebním bytě. Jistě máte hlad. Najděte v bytě ledničku - tam vás čeká svačina. ----------------------------------------------------------------------------Při vyhodnocování odpovědi se objevil problém: Metoda ještě není hotova. at cz.pecinovsky.adv_framework.test_util.gamet.GameStepTest.describeError(GameStepTest.java:418) at cz.pecinovsky.adv_framework.test_util.gamet.GameStepTest.checkState(GameStepTest.java:124) atd. Caused by: java.lang.UnsupportedOperationException: Metoda ještě není hotova. at org.edupgm.adventure.common.rup14p.OfficialApartmentGame.getBag(OfficialApartmentGame.java:127) at cz.pecinovsky.adv_framework.test_util.gamet.GameStepTest.describeError(GameStepTest.java:401) ... 21 more První sada výpisů zásobníku nám sice moc neřekne, ale na konci se dozvíme, že prvotní příčinou bylo vyhození výjimky UnsupportedOperationException metodu getBag() ve třídě hry. Tato metoda má vrátit objekt batohu. Jenomže třídu batohu jsme ještě nedefinovali – jdeme to napravit.
6.12 Doplnění batohu Nejprve zkopírujeme z balíčku empty_classes třídu EmptyBag a hned ji také přejmenujeme. Batohem v této hře budou hráčovy ruce, tak třídu batohu přejmenujeme na Hands. Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Batoh bude ve hře jediný, a proto hned upravíme definici třídy podle doporučení návrhového vzoru Jedináček:
Definujeme statickou konstantu typu batohu (tj. Hands) a pojmenujeme ji např. SINGLETON (můj oblíbený název pro instance jedináčků).
Instanci hned v deklaraci inicializujeme zavoláním bezparametrického konstruktoru.
Definujeme konstruktor jako soukromý. Definujeme tovární metodu getInstance(), která bude vracet instanci našeho jedináčka. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 115 z 240
116
Návrh semestrálního projektu a jeho rámce – Adventura
Základ je tedy hotov a půjdeme se podívat, jaké metody má batoh implementovat.
Metoda getCapacity() První metoda požaduje, aby batoh vrátil svoji kapacitu. Když jsme si řekli, že v naší hře budou hypotetický batoh zastupovat ruce, prohlásíme, že kapacita batohu bude rovna 2. Protož v programu se nemají vyskytovat magické hodnoty, zavedeme statickou konstantu CAPACITY s hodnotou 2 a tělo metody definujeme tak, aby vracelo hodnotu této konstanty.
Metody getObjects() a initialize() Druhou požadovanou metodou je metoda getObjects(), která má vrátit kolekci objektů v batohu. Deklarujeme proto kolekci objektů, kterou nazveme (jak jinak) objects a v těle metody vrátíme tuto kolekci zabalenou do neměnné kolekce, jak už jsme to několikrát dělali. Hned bychom si měli vzpomenout na to, jak jsme před nedávnem inicializovali prostory a i v této třídě definujeme metodu initialize(), která vytvoří prázdnou kolekci a uloží ji do příslušné proměnné. V této hře nezačínáme s batohem, v němž už něco je, takže nemusíme v rámci inicializace vkládat potřebné objekty do batohu. Kdo z vás takou hru vytváří, může se inspirovat inicializací stejnojmenné kolekce ve třídě Rooms. Jakmile vytvoříme inicializační metodu, hned zaběhneme do třídy správce příkazů (třída ACommand) a do její inicializační metody přidáme volání inicializace batohu.
Metoda getBag() ve třídě hry Vrátíme se do třídy hry a doplníme tělo metody getBag() tak, aby vracela instanci právě vytvořeného batohu, tj. zadáme do něj příkaz: return Hands.getInstance(); Při té příležitosti upravíme také typ návratové hodnoty této metody na Hands ze stejných důvodů, z nichž jsme upravovali typ návratových hodnot některých dříve definovaných metod.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 116 z 240
117
Kapitola 6: Vytváříme svět hry
6.13 Test Po následném spuštění testu hry se v okně pro standardní výstup objeví základní test správce scénářů, po něm zpráva o provedení startovacího kroku a za ní zpráva: ============================================================================= Při testu následujícího, tj. 1. kroku: Jdi Koupelna se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Přesunul(a) jste se do místnosti: Koupelna» Přišlo: «Tento příkaz neznám.» Vypadá to, že definici částí programu potřebných pro spuštění hry a vyhodnocení stavu hry po provedení startovacího kroku jsme zvládli, protože chyba se objevila až při vyhodnocování následujícího příkazu. Můžeme se tedy pustit do definice jednotlivých příkazů.
6.14 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 5.10 Co jsme prozatím naprogramovali na straně 94, jsme přidali následující:
Máme
definovanou třídu Appartment, jejíž instance reprezentuje svět hry (v našem příkladu služební byt) a slouží zároveň jako správce jednotlivých prostor.
Definovali třídu Room, jejíž instance představují prostory hry – v našem příkladu pokoje služebního bytu.
Definovali jsme třídu Thing, jejíž instance představují objekty, které se mohou vyskytovat v jednotlivých prostorech a/nebo v batohu.
Definovali
jsme třídu Hands, jejíž instance představují hráčův batoh, kterým jsou v případě naší hry jeho ruce.
Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 6.1.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 117 z 240
118
Návrh semestrálního projektu a jeho rámce – Adventura
Obrázek 6.1 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující svět hry a jeho části
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A06z_GameWorld.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 118 z 240
119
Kapitola 7: Vytvoření prvních povinných příkazů 7.
Vytvoření prvních povinných příkazů
Kapitola 7 Vytvoření prvních povinných příkazů
7.1
Co se v kapitole naučíme V této kapitole se zaměříme na vytvoření povinných příkazů, tj. příkazů pro vyvolání nápovědy, přesun z prostoru do prostoru, zvednutí a položení objektu a předčasné ukončení. V této kapitole se budeme věnovat prvním třem, zbylé dva odložíme do kapitoly příští.
Jak sjednotit postup pro většinu her
Chceme-li nejprve definovat mateřské třídy základních příkazů, nemusí být rozumné postupovat nadále dosavadním způsobem, protože mnohé scénáře jsou koncipovány tak, že některý z povinných příkazů se použije až poté, co se použije některý z nestandardních příkazů. Abychom mohli začít s definicí povinných příkazů naším „oblíbeným“ systémem test – oprava – test – oprava, potřebovali bychom mít zaručeno, že ve scénáři budou nejprve kroky používající povinné příkazy a až poté kroky s případnými nestandardními příkazy. V některých případech by ale takovýto požadavek boural celou koncepci navržené hry. Existuje ale způsob, jak toho dosáhnout, aniž bychom museli koncepci hry upravovat – můžeme definovat nový scénář, který bude těmto požadavkům vyhovovat. Po tomto scénáři budeme požadovat, aby obsahoval následující kroky v uvedeném pořadí (to abychom sjednotili postup vytváření příkazů): 1. Startovací krok 2. Vypsání nápovědy
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 119 z 240
120
Návrh semestrálního projektu a jeho rámce – Adventura
3. Přesun do sousedního prostoru 4. Zvednutí objektu v tomto prostoru 5. Položení zvednutého objektu 6. Předčasné ukončení hry
Takovýto scénář lze definovat téměř vždy. Pokud by se v dané hře nacházel hráč v prostoru, z nějž na počátku nevede žádný východ a je potřeba nejprve něco udělat, aby se východ vytvořil, lze ve hře definovat nějaký pomocný prostor, který bude s výchozím prostorem sousedit a jehož jediným účelem bude, aby se do něj mohl hráč v rámci těchto testů přesunout. Nebude-li ve výchozím prostoru žádný objekt, který by bylo možno zvednout, můžeme do něj dodatečně nějaký pomocný objekt vložit, anebo přecházení do sousedních prostorů opakovat tak dlouho, až dorazíme do prostoru, kde je co sebrat. (Přimlouval bych se ale za první řešení.) Jediným případem, kdy by mohl požadavek na vytvoření takovéhoto scénáře kolidovat s koncepcí hry, by byly hry, v nichž hráč začíná s plným batohem. Pak by bylo potřeba buď zvětšit kapacitu batohu, anebo prohodit kroky 4 a 5. Moje zkušenost ale říká, že takového návrhy scénářů vznikají zcela výjimečně – z těch mnoha set scénářů, které mí studenti doposud vytvořili, si na takový nepamatuji. Pokud tedy někdo z vás takováto scénář navrhl a nechce se mu zvětšovat kapacitu batohu či jinak měnit koncepci své hry, musí v následujícím výkladu brát část o vytváření příkazů pro manipulaci s objekty pouze jako inspirativní, protože jeho program se s ním bude bavit trochu jinak.
7.2
Definice nového scénáře
Chceme-li zavést nový scénář, musíme nejprve definovat jeho kroky. Za chybový scénář proto vložíme statický inicializační blok, který bude znovu inicializovat číslování kroků a za něj pak přidáme pole s kroky pro tento scénář. V naší demonstrační hře by definice tohoto pole (nazval jsem je REQUIRED_STEPS) vypadala podle výpisu 7.1. Výpis 7.1:
Definice pole REQUIRED_STEPS ve třídě OfficialApartmentManager
14 static { ScenarioStep.setIndex(1); } 15 16 17 /*************************************************************************** 18 * Kroky scénáře určeného pro prověření povinných příkazů, 19 * přesněji příkazů pro přechod do prostoru, zvednutí a položení objektu, 20 * vypsání nápovědy a pro předčasné ukončení hry. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 120 z 240
Kapitola 7: Vytvoření prvních povinných příkazů
121
21 */ 22 private static final ScenarioStep[] REQUIRED_STEPS = 23 { 24 START_STEP, 25 26 new ScenarioStep(tsHELP, pHELP, 27 zNÁPOVĚDA 28 , 29 PŘEDSÍŇ, 30 new String[] { LOŽNICE, OBÝVÁK, KOUPELNA }, 31 new String[] { BOTNÍK, DEŠTNÍK }, 32 new String[] {} 33 ), 34 35 new ScenarioStep(tsMOVE, pJDI + " " + KOUPELNA, 36 zPŘESUN + KOUPELNA 37 , 38 KOUPELNA, 39 new String[] { PŘEDSÍŇ }, 40 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 41 new String[] {} 42 ), 43 44 new ScenarioStep(tsPICK_UP, pVEZMI + " " + BRÝLE, 45 zZVEDNUTO + BRÝLE 46 , 47 KOUPELNA, 48 new String[] { PŘEDSÍŇ }, 49 new String[] { UMYVADLO, ČASOPIS }, 50 new String[] { BRÝLE } 51 ), 52 53 new ScenarioStep(tsPUT_DOWN, pPOLOŽ + " " + BRÝLE, 54 zPOLOŽENO + BRÝLE 55 , 56 KOUPELNA, 57 new String[] { PŘEDSÍŇ }, 58 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 59 new String[] {} 60 ), 61 62 new ScenarioStep(tsEND, pKONEC, 63 zKONEC 64 , 65 KOUPELNA, 66 new String[] { PŘEDSÍŇ }, 67 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 68 new String[] {} 69 ), 70 71 }; 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 121 z 240
122
Návrh semestrálního projektu a jeho rámce – Adventura
7.3
Úprava definice konstruktoru
Kroky máme definovány. Teď ještě musíme zařídit, aby se vytvořil scénář obsahující tyto kroky. Musíme proto do konstruktoru přidat příkaz, který vytvoření takovéhoto scénář zabezpečí. A protože tomuto scénáři musíme přiřadit jméno, bylo by vhodné definovat pro ně konstantu, na niž se budeme moci v dalším programu odvolávat. Definujeme proto statickou konstantu: private static final String REQUIRED_STEPS_SCENARIO_NAME = "REQUIRED"; Nyní již zbývá pouze přidat do definice konstruktoru požadavek na přidání dalšího scénáře. Upravený konstruktor pak bude mít tvar z výpisu 7.2. Výpis 7.2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Upravená definice konstruktoru třídy OfficialApartmentManager
/*************************************************************************** * Vytvoří instanci představující správce scénářů hry. */ private OfficialApartmentManager() { super(AUTHOR_NAME, AUTHOR_ID, CLASS);
}
7.4
addScenario(HAPPY_SCENARIO_NAME, TypeOfScenario.scHAPPY, HAPPY_SCENARIO_STEPS); addScenario(MISTAKE_SCENARIO_NAME, TypeOfScenario.scMISTAKES, MISTAKE_SCENARIO_STEPS); addScenario(REQUIRED_STEPS_SCENARIO_NAME, TypeOfScenario.scGENERAL, REQUIRED_STEPS); seal();
Úprava definice metody main(String[])
Konstruktor jsme upravili. Nyní musíme upravit ještě metodu main(String[]), která spouští testy. Zakomentujeme příkaz pro test hry, který prověřuje chod hry podle úspěšného a chybového scénáře a odkomentujeme příkaz pro testování hry podle scénáře se zadaným názvem. Do příkazu zadáme název scénáře s povinnými kroky, který jsme před chvílí definovali. Výsledná podoba metody main(String[]) je ve výpisu 7.3. Výpis 7.3:
Upravená definice metody main(String[]) ve třídě OfficialApartmentManager
1 public static void main(String[] args) 2 { 3 // //Otestuje, zda správce scénářů a jeho scénáře vyhovují požadavkům 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 122 z 240
123
Kapitola 7: Vytvoření prvních povinných příkazů 4 5 6 7 8 9 10 11 12 13 14 15 16
//
MANAGER.autoTest();
// //
//Testování hry prováděné postupně podle obou povinných scénářů MANAGER.testGame(); //Testování hry dle scénáře se zadaným názvem či indexem MANAGER.testGameByScenario(REQUIRED_STEPS_SCENARIO_NAME);
// // }
7.5
//Odehrání hry dle scénáře se zadaným názvem MANAGER.playGameByScenario("???"); System.exit(0);
Definice příkazu pro nápovědu
Při kontrolním spuštění testu bychom měli obdržet obdobnou zprávu, jakou jsme obdrželi na konci minulé kapitoly. Testovací program zadal hře příkaz pro vypsání nápovědy a hra oznámila, že takový příkaz nezná. Ale to je správně, protože jsme jej ještě nedefinovali. Pojďme to tedy napravit.
Zkopírování kostry třídy Jak si jistě domyslíte, začátek definice požadovaného příkazu bude stejný jako začátek definice všech předchozích tříd: otevřeme balíček empty_classes, zkopírujeme z něj třídu EmptyACommand do našeho balíčku a hned ji také přejmenujeme na CommandMove. Po otevření zdrojového kódu třídy v okně editoru nejprve smažeme automaticky vkládaný příkaz importující třídy z balíčku, odkud jsme danou třídu zkopírovali, tj. příkaz: import cz.pecinovsky.adv_framework.empty_classes.*; Hned poté projdeme zdrojový kód nově vytvořené třídy a podíváme se, co bychom mohli hned, bez přemýšlení upravit a definovat.
Rodičovská třída První, na co narazíme, je chyba v hlavičce třídy, která vznikla smazáním příkazu import. Naše třída je totiž definována jako potomek třídy EmptyACommand, což, jak víme, není pravda. Smazání slovíčka Empty v názvu rodičovské třídy tuto chybu vyřeší, protože víme, že naše třída je potomkem třídy ACommand, která je ve stejném balíčku, takže žádný import nepotřebuje. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 123 z 240
124
Návrh semestrálního projektu a jeho rámce – Adventura
Při dalším procházení zdrojového kódu narazíme na definici konstruktoru, která obsahuje volání konstruktoru rodičovského podobjektu, jemuž předává v parametrech název a popis daného objektu. Připomeneme si ve správci scénářů, že název tohoto příkazu je uložen v konstantě Texts.pHELP a můžeme konstruktor hned upravit do podoby ve výpisu 7.4. Výpis 7.4:
Definice konstruktoru instancí třídy CommandHelp
17 CommandHelp() 18 { 19 super (pHELP, 20 "Vypíše nápovědu - názvy a popisy všech příkazů."); 21 } Editor sice bude tvrdit, že danou instanci nezná, nicméně při stisku CTRL+SPACE nám ji nabídne a po potvrzení vloží kompletní odkaz do kódu. Kompletní odkaz obsahující úplný název oslovované třídy včetně jejího balíčku ale nevypadá hezky, tak jej můžeme zkopírovat do schránky využít jej při definici příslušného statického importu: import static cz.pecinovsky.adv_framework.test_util.default_game.Texts.*;
Metoda execute(String...) Budeme-li pokračovat v analýze zkopírovaného kódu, narazíme na definici metody execute(String...), která přebíjí abstraktní metodu rodičovské třídy. Tato metoda bude mít na starosti provedení příkazu. Pojďme se podívat, jak ji definovat. Příkaz pro nápovědu by měl být poměrně jednoduchý: zeptáme se hry na všechny její příkazy a pak postupně vypíšeme názvy jednotlivých příkazů spolu s jejich popisem. Onoho postupného vypsání dosáhneme tak, že získanou kolekci příkazů převedeme na datovod, jehož jednotlivé prvky (příkazy) převedeme na řetězce složené z názvu daného příkazu následovaného odřádkováním a popisem příkazu. Datovod těchto řetězců nám pak poslouží k jejich složení do výsledného řetězce s odpovědí na příkaz nápovědy. K tomuto složení využijeme kolektoru, který získáme z knihovní třídy Collectors zavoláním její metody joining(CharSequence, CharSequence, CharSequence), jejíž první parametr specifikuje oddělovač jednotlivých položek přišedších vstupním datovodem ve výsledném řetězci, druhý zadává text uváděný před složeninou a třetí pak text vkládaný za složeninu. Jako oddělovač použijeme dvojité odřádkování, aby mezi popisy jednotlivých příkazů zůstal volný řádek, úvodní text si najdeme ve scénáři a závěrečný text zadáme jako prázdný řetězec.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 124 z 240
125
Kapitola 7: Vytvoření prvních povinných příkazů
Definici metody execute(String...) navrženou podle předchozího popisu si můžete prohlédnout ve výpisu 7.5. Výpis 7.5: 1 2 3 4 5 6 7 8 9 10 11
Definice metody execute(String...) ve třídě CommandHelp
@Override public String execute(String... arguments) { OfficialApartmentGame game = OfficialApartmentGame.getInstance(); Collection
Test Spustíme opět test, abychom se přesvědčili, že jsme vše naprogramovali správně. Bohužel, test dopadne stejně jako minule a hra bude tvrdit, že zadaný příkaz nezná. Chyba je v tom, že jsme daný příkaz sice definovali, ale neřekli jsme o tom správci příkazů. V pasáži Vytvoření jednotlivých příkazů na straně 84 jsme si říkali, že by bylo vhodné jednotlivé příkazy vytvořit rovnou v konstruktoru jejich správce, aby o nich věděl již od svého narození. Správce příkazů naší současné hry je ale objekt třídy – musíme proto vložit příkazy zabezpečující vytvoření jednotlivých příkazů do konstruktoru třídy. Číst kódu s naším prvním příkazem tedy bude mít tvar: //############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== static { new CommandHelp(); } Po zadání příkazu na vytvoření instance příkazu hry „rozsvítí“ NetBeans u příkazu žárovičku upozorňující na to, že volaná metoda vrací hodnotu, kterou v programu ignorujeme. My ale víme, že vytvořený příkaz nemusíme nikam ukládat, protože součástí konstruktoru rodičovského podobjektu je i vložení dvojice [název; příkaz] do mapy NAME_2_COMMAND pamatující si všechny vytvořené příkazy společně s jejich názvy (viz výpis 5.3 na straně 84). Když po této úpravě znovu spustíme test, dozvíme se, že:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 125 z 240
126
Návrh semestrálního projektu a jeho rámce – Adventura
Ukončení bylo způsobeno vyhozením výjimky: cz.pecinovsky.adv_framework.test_util.comon.TestException: Při vykonávání příkazu: «?» vyhodila hra výjimku at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyScenarioStep(GameTRunTest.java:300) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.executeScenario(GameTRunTest.java:200) atd. Caused by: java.lang.UnsupportedOperationException: Metoda ještě není hotova. at org.edupgm.adventure.common.rup14p3a.OfficialApartmentGame.getAllCommands(OfficialApartmentGame.java:136) at org.edupgm.adventure.common.rup14p3a.CommandHelp.execute(CommandHelp.java:78) Jinými slovy: běh programu zhavaroval na tom, že ve třídě hry ještě není definována metoda getAllCommands(). Pojďme tedy na ni
Doplnění těla metody getAllCommands() ve třídě hry Po spuštění testu se dozvíme, že náš příkaz nechodí, protože ve třídě hry ještě není definována metoda getAllCommands(). Budeme postupovat stejně, jako u většiny metod této třídy: prohlásíme, že starost o příkazy je věcí správce příkazů a delegujeme zodpovědnost na něj. Současně upravíme signaturu metody tak, že typový parametr se žolíkem nahradíme přímo společným typem našich příkazů, kterým je typ ACommand. Po těchto úpravách získá metoda podobu z výpisu 7.6. Výpis 7.6: 1 2 3 4 5 6 7 8 9 10
Definice metody getAllCommands() ve třídě OfficialApartmentGame
/*************************************************************************** * Vrátí kolekci všech příkazů použitelných ve hře. * * @return Kolekce všech příkazů použitelných ve hře */ @Override public Collection
Doplnění těla metody getAllCommands() ve třídě správce příkazů Ve třídě hry jsme se sice problému s nedefinovanou metodou zbavily, ale pouze tak, že jsme nutnost definovat tuto metodu delegovali na správce příkazů. Pojďme se tedy podívat, jak bychom ji mohli vyřešit tam. Správce příkazů (objekt třídy ACommand) nemá definovanou kolekci či jiný kontejner, v němž by si pamatoval definované příkazy. Má však mapu poskytující konverze z názvu příkazů na vlastní příkaz. Součástí dvojic uložených v mapě jsou i vlastní příkazy. Můžeme tedy o ně požádat mapu. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 126 z 240
127
Kapitola 7: Vytvoření prvních povinných příkazů
Mapa je na takovéto žádosti připravena a definuje pro ně metodu values(), která vrátí kolekci všech hodnot, kterými jsou v tomto případě naše příkazy. Tato kolekce je ale pouze zvláštním způsobem pohledu na mapu. Jakmile změníme něco v mapě, změní se i obsah této kolekce a naopak, jakmile z kolekce nějakou hodnotu odebereme, odebere se i příslušná dvojice v mapě. (Přidávat do kolekce nemůžeme, protože kolekce neumožňuje zadat k přidávané hodnotě také její klíč.) Chceme-li zabránit tomu, aby nějaký záludný program naši mapu změnil, musíme tuto kolekci opět zabalit a vrátit jejího neměnitelného náhradníka. Definici metody getAllCommands() navrženou podle předchozího popisu si můžete prohlédnout ve výpisu 7.7. Všimněte si, že metoda je definována jako statická (je to metoda objektu třídy) a není veřejná. Její viditelnost je nastavena na package private, protože je určena pouze pro vnitřní potřebu tříd definujících danou hru. Výpis 7.7: 1 2 3 4 5 6 7 8 9
Definice metody getAllCommands() ve třídě ACommand
/*************************************************************************** * Vrátí kolekci všech příkazů použitelných ve hře. * * @return Kolekce všech příkazů použitelných ve hře */ static Collection
Test Když po této úpravě znovu spustíme test, dozvíme se, že: ============================================================================= Při testu následujícího, tj. 2. kroku: Jdi Koupelna se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Přesunul(a) jste se do místnosti: Koupelna» Přišlo: «Tento příkaz neznám.» Jinými slovy: příkaz pro přesun do sousedního prostoru prošel bez připomínek a testovací program se zasekl až při vyhodnocování následujícího kroku, kterým je přesun do sousedního prostoru.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 127 z 240
128
Návrh semestrálního projektu a jeho rámce – Adventura
7.6
Definice příkazu pro přesun z aktuálního prostoru do zadaného sousedního prostoru
Poslední test nám prozradil, že naše hra ještě neumí vykonat příkaz Jdi. Tak to napravíme. Tentokrát nebudeme kopírovat kostru třídy z balíčku empty_classes, protože si z pasáží Zkopírování kostry třídy na straně 123 a Rodičovská třída na straně 123 jistě pamatujete, že jsme pak museli zanášet několik oprav. Využijeme toho, že jsme již definovali třídu CommandHelp a zkopírujeme ji, přičemž výslednou třídu pojmenujeme CommandMove. Jediné, co budeme muset ještě upravit (samozřejmě vedle výkonné metody execute(String…) je tělo konstruktoru, kde zadáme správný název (najdeme jej ve scénářích) a popis daného příkazu. Hned také zaběhneme do třídy ACommand a v konstruktoru správce příkazů, kterým je statický konstruktor této třídy (tj. správcem příkazů je přece objekt třídy) přidáme příkaz pro vytvoření instance právě definované třídy CommandMove. (Možnou podobu konstruktoru si můžete prohlédnout ve výpisu 7.8.) Výpis 7.8: 10 11 12 13 14 15 16 17 18 19
Upravená definice konstruktoru ve třídě CommandMove
/*************************************************************************** * Vytvoří novou instanci daného příkazu. */ CommandMove() { super (pJDI , "Metoda přesune hráče do sousední místnosti zadané v parametru." + "\nVyžaduje však, aby tato místnost byla sousedem aktuální místnosti," + "\nprotože jinak přesun neprovede a bude příkaz považovat za chybný"); }
Metoda execute(String...) Budeme-li pokračovat v analýze zkopírovaného kódu, narazíme na definici metody execute(String...), která přebíjí abstraktní metodu rodičovské třídy. Tato metoda bude mít na starosti provedení příkazu. Smažeme její zkopírované tělo a podíváme se, jak ji definovat. Existence parametru Nejprve musíme zjistit, jestli uživatel vůbec nějaký cílový prostor zadal. Zeptáme se proto, jestli má vektor parametrů alespoň dva prvky (vždy musí být alespoň jeden, protože nultým prvkem je název zadaného příkazu). Není-li tomu tak, vrátíme příslušnou odpověď naplánovanou ve scénáři pro tento případ. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 128 z 240
129
Kapitola 7: Vytvoření prvních povinných příkazů
Je-li parametr, tj. název cílového prostoru, zadán (je to první prvek pole), zapamatujeme si jej (vytvoříme pro něj proměnnou destinationName), protože s ním budeme v dalším kódu pracovat. Korektnost parametru Takže nyní víme, že cílový prostor je zadán. Potřebujeme ale zjistit, jestli je zadán korektně, tj. jestli je to jeden ze sousedních prostorů aktuálního prostoru, protože nikam jinam tento příkaz hráče přesunout nesmí. Zeptáme se proto aktuálního prostoru, zda je zadaný prostor mezi jeho sousedy. K tomu ale potřebujeme zjistit, který prostor je právě aktuální. Jak si ale možná pamatujete, to by měl vědět svět hry, v našem případě instance třídy Aparmtent. Když získáme aktuální prostor, zjistíme, že nemá definovanou metodu, jejímž zavoláním mohli jeho souseda zjistit. Umí ale vrátit kolekci svých aktuálních sousedů a interfejs INamed umí v kolekci pojmenovaných objektů najít objekt se zadaným jménem. Musíme jenom mít na paměti, že metoda vrací objekt typu Optional, s nímž jsme se již setkali např. ve výpisu 6.9 na straně 113. Zjistíme-li, že mezi sousedy aktuálního prostoru není prostor se zadaným názvem, vrátíme text naplánovaný pro tuto situaci ve scénáři. Přesun do cílového prostoru Teď už by snad mělo být vše v pořádku a můžeme realizovat vlastní přesun a podat o něm zprávu. Jinými slovy, potřebujeme změnit aktuální prostor. Informaci o tom, který prostor je aktuální, udržuje svět hry. Měli bychom mu tedy změnu nahlásit. Chybou je, že není jak. Řešení je jednoduché: pro objekt světa hry definujeme metodu setCurrentSpace(Room), jež mu poví, který prostor bude od této chvíle tím aktuálním. Předpokládám, že definici této metody nemusím ukazovat. Jenom bych připomněl, že metoda by neměla být public, ale pouze package private, aby ji mohly volat pouze ostatní objekty našeho balíčku a nebylo možno aktuální prostor změnit zvenku. Nyní je již aktuální prostor změněn. Najdeme si ve scénáři, jak máme hráči tuto skutečnost oznámit, a vrátíme příslušný text. Tím je definice hotova. Můžete si ji prohlédnout ve výpisu 7.9. Výpis 7.9:
Definice metody execute(String...) ve třídě CommandMove
1 /*************************************************************************** 2 * Přesune hráče do sousední místnosti zadané v parametru. 3 * Vyžaduje však, aby tato místnost byla sousedem aktuální místnosti, 4 * protože jinak přesun neprovede a bude příkaz považovat za chybný. 5 * 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 129 z 240
130 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Návrh semestrálního projektu a jeho rámce – Adventura * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zCIL_NEZADAN; } String destinationName = arguments[1]; Room currentRoom = Apartment.getInstance().getCurrentArea(); Optional
Test Když po těchto úpravách spustíme test, dozvíme se, že: ============================================================================= Při testu následujícího, tj. 2. kroku: Vezmi Brýle se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Vzal(a) jste objekt: Brýle» Přišlo: «Tento příkaz neznám.» Jinými slovy: příkaz pro přesun do sousedního prostoru prošel bez připomínek a testovací program se zasekl až při vyhodnocování následujícího kroku, kterým je zvednutí objektu.
7.7
Definice příkazu pro zvednutí objektu v aktuálním prostoru a jeho přemístění do batohu
Předpokládám, že úvodní kroky k definici příkazu (tj. zkopírování třídy některého dříve definovaného příkazu, přejmenování této kopie na CommandPutDown, definice konstruktoru a zařazení příkazu vytvářejícího instanci tohoto příkazu do konstruktoru správce příkazů) nemusím nijak podrobně rozebírat, protože jsme je už dělali několikrát, takže je jistě budete ovládat. Vrhneme se proto rovnou na metodu execute(String...). 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 130 z 240
Kapitola 7: Vytvoření prvních povinných příkazů
131
Existence parametru Nejdříve se musíme (stejně jako minule) podívat, jestli uživatel nezapomněl zadat parametry příkazu, tj. název objektu, který mám přemístit. To už jsme dělali minule, takže to tu nebudu opakovat. Kdo zapomněl, ví, kde má hledat návod.
Korektnost parametru S testem korektnosti zadaného parametru to bude tentokrát poněkud těžší, protože musíme zjistit nejenom to, že se daný objekt nachází v aktuálním prostoru, ale také to, že je zvednutelný. Přitom příčin jeho nezvednutelnosti může být více:
Může se jednat o principiálně nezvednutelný objekt (např. budovu). Může se jednat o objekt, který je sice teoreticky zvednutelný, ale do batohu se nám již nevejde.
Principy další práce příkazu Budeme tedy muset postupně otestovat obě dvě. Abychom ale mohli testovat, musíme nejprve získat odkaz na aktuální prostor a od něj pak kolekci jeho objektů. Z té se pak pokusíme získat požadovaný objekt, a pokud se nám to podaří, můžeme batoh požádat, aby objekt převzal. To se může, ale také nemusí podařit. Je-li batoh plný, tak převzetí objektu odmítne. Proto bychom neměli odebírat objekt z prostoru dříve, než se přesvědčíme, že jej bude batoh ochoten přijmout, protože jinak bychom museli nepřijatý objekt do prostoru vracet.
Úpravy definice třídy prostorů Protože víme, že není vhodné, abychom se hrabali v cizích kolekcích (dáváme si přece práci s tím, aby se to nenechavcům žádajícím naše kolekce nemohlo podařit), budeme muset změny obsahu těchto kolekcí definovat tak, že do tříd obou kontejnerů přidáme potřebné metody. Do třídy Room přidáme metodu Thing getObject(String name) která nám vrátí odkaz na objekt s daným názvem uložený v osloveném kontejneru (je-li v něm více objektů se shodným názvem, vrátí libovolný z nich), a metodu void removeObject(Thing) která zadaný objekt odeber ze své kolekce objektů. Možnou verzi zdrojového kódu obou metod najdete ve výpisu 7.10. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 131 z 240
132
Návrh semestrálního projektu a jeho rámce – Adventura
Výpis 7.10: Definice metod getObject(String) a removeObject(Thing) ve třídě Room 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
/*************************************************************************** * Vrátí odkaz na objekt s daným názvem uložený v osloveném kontejneru * (je-li v něm více objektů se shodným názvem, vrátí libovolný z nich). * * @param objectName Název požadovaného objektu * @return Odkaz na hledaný objekt nebo {@code null} v případě, * kdy žádný objekt s takovým názvem v místnosti není */ Thing getObject(String objectName) { Thing result = INamed.get(objectName, objects).orElse(null); return result; } /*************************************************************************** * Odebere zadaný objekt ze své kolekce objektů. * * @param object Odebíraný objekt */ void removeObject(Thing object) { objects.remove(object); }
Jak si můžete všimnout na řádku 11, obdržený výsledek typu Optional
Úpravy definice třídy batohu Obdobně budeme postupovat i se třídou Hands, jejíž instance představuje hypotetický batoh (v naší hře ruce). Ta musí umět přidat objekt do své kolekce, ale před tím ještě musí prověřit, zda se tam vůbec vejde. Definujeme v ní proto metodu tryAddObject(Thing), kterou požádáme batoh o přidání objektu, a batoh nám vrátí informaci o tom, zda objekt přijal. Možnou verzi zdrojového kódu této metody najdete ve výpisu 7.11. Výpis 7.11: Definice metody tryAddObject(Thing) ve třídě Hands 1 /*************************************************************************** 2 * Vejde-li se zadaný objekt do batohu, přidá jej a vrátí zprávu o výsledku. 3 * 4 * @param object Objekt, který se má přidat do batohu 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 132 z 240
133
Kapitola 7: Vytvoření prvních povinných příkazů 5 * @return Zpráva o výsledku: {@code true} = byl přidán, 6 * {@code false} = nebyl přidán 7 */ 8 boolean tryAddObject(Thing object) 9 { 10 if (object.getWeight() > remains) { 11 return false; 12 } 13 objects.add(object); 14 remains -= object.getWeight(); 15 return true; 16 }
Definice metody execute(String...) definovaného příkazu Nyní již máme všechno připraveno a můžeme definovat to nejdůležitější – tělo metody execute(String...). Principy její práce jsme si rozebrali v pasáži Principy další práce příkazu na straně 131, takže můžeme začít rovnou tvořit. Možnou výslednou podobu kódu si můžete prohlédnout ve výpisu 7.12. Výpis 7.12: Definice metody execute(String...) ve třídě CommandPickUp 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
/*************************************************************************** * Přesune objekt zadaný v parametru z aktuálního prostoru do batohu. * Vyžaduje však, aby objekt byl zvednutelný a do batohu se vešel. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zOBJEKT_NEZADAN; } String objectName = arguments[1]; Room currentRoom = Apartment.getInstance().getCurrentArea(); Thing object = currentRoom.getObject(objectName); if (object == null) { return zNENÍ_OBJEKT + objectName; } Hands bag = Hands.getInstance(); if (object.getWeight() >= bag.getCapacity()) { return zTĚŽKÝ_OBJEKT + objectName; } boolean added = bag.tryAddObject(object); if (added) { currentRoom.removeObject(object); return zZVEDNUTO + objectName; }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 133 z 240
134
Návrh semestrálního projektu a jeho rámce – Adventura
45 46 47 48 }
else { return zBATOH_PLNÝ; }
Test Po spuštění testu zjistíme, že právě definovaný příkaz se úspěšně zhostil svého úkolu a test se zarazil až na příkazu pro položení objektu. ============================================================================= Při testu následujícího, tj. 4. kroku: Polož Brýle se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Položil(a) jste objekt: Brýle» Přišlo: «Tento příkaz neznám.» Při jeho definici vám ale budu chtít ukázat další úpravy architektury, takže mu věnujeme samostatnou kapitolu.
7.8
Co jsme prozatím naprogramovali
Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 6.14 Co jsme prozatím naprogramovali na straně 117, jsme přidali následující:
Definovali
jsme třídu CommandHelo, jejíž instance je zodpovědná za správnou reakci na příkaz k vypsání nápovědy.
Definovali
jsme třídu CommandMove, jejíž instance je zodpovědná za správnou reakci na příkaz přesunu do sousedního prostoru.
Definovali jsme třídu CommandPickUp, jejíž instance je zodpovědná za správnou reakci na příkaz zvednutí objektu v aktuálním prostoru a jeho přesun do batohu. Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 7.1.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 134 z 240
Kapitola 7: Vytvoření prvních povinných příkazů
135
Obrázek 7.1 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující první tři povinné příkazy hry
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A07z_FirstRequiredCommands.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 135 z 240
136 8.
Návrh semestrálního projektu a jeho rámce – Adventura Definice společného rodiče kontejnerů objektů
Kapitola 8 Definice společného rodiče kontejnerů objektů
8.1
Co se v kapitole naučíme V této kapitole začneme doplňovat příkaz pro zvednutí objektu a při jeho definici si ukážeme, že by se nám mohlo hodit zavedení společného rodiče pro kontejnery objektů. V závěru kapitoly doplním příkaz pro předčasné ukončení hry a tím úspěšně završíme test doplněného scénáře pro prověření implementace povinných příkazů.
Definice příkazu pro položení objektu
Definice příkazu pro položení objektu bude velmi podobná definici příkazu pro jeho zvednutí. Začátek její definice bude úplně stejný: zkopírujeme třídu (nejlépe třídu CommandPickUp) a vytvořenou kopii nazveme CommandPutDown. Upravíme její konstruktor a přidáme příkaz pro vytvoření instance této třídy k předchozím třem příkazům ve statickém konstruktoru třídy ACommand. Jak jistě odhadnete, jedná se vlastně pouze o drobně upravenou podobu předchozího příkazu, v níž se změní zdroj a cíl přesunu objektu. I tentokrát musíme definovat potřebné metody ve třídách Room a Hands a poté doplnit definici metody execute(String...). Definice je o něco jednodušší, protože při ukládání objektu do prostoru nemusíme testovat, zda se tam objekt vejde. Je tedy zřejmé, že bychom měli v každé ze tříd Room a Hands definovat ekvivalenty metod, které jsme při definici předchozího příkazu definovali v jejich protějšku. Když se ale podíváte do diagramu tříd interfejsů, které tyto třídy implementují (viz obrázek 4.1 na straně 75), zjistíte, že tyto interfejsy mají společného prarodiče, kterým je interfejs IObjectContainer. Mohlo by nás proto napad49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 136 z 240
Kapitola 8: Definice společného rodiče kontejnerů objektů
137
nout, jestli bychom neměli těmto třídám také definovat společného třídního předka, do nějž bychom vytkli vše, co mají obě třídy společné. Pojďme zjistit, nakolik by to bylo výhodné.
8.2
Co bychom mohli do společného předka vytknout
Při úvahách o výhodnosti definice společného předka musíme vždy přemýšlet nad tím, které atributy a metody bychom do něj mohli vytknout. Společného předka jsme zde již definovali – byla jím třída ACommand, která je společným rodičem všech příkazů. Tehdy jsme do ní vytkli metody getName() a getDescription() spolu s příslušnými atributy. Rozhodnutí o společném předku tehdy ještě podpořilo to, že objekt třídy může efektivně vystupovat jako správce instancí dané třídy nezávisle na tom, jedná-li se o vlastní instance dané třídy nebo o instance některého z jejích potomků. V případě kontejnerů objektů by se do společného předka daly vytknout všechny doposud definované metody, tj.:
Metoda getObjects() vracející neměnnou kolekci všech objektů v daném kontejneru (prostoru či batohu). Spolu s touto metodou bychom ale měli vytknout do rodičovského podobjektu i atribut objects, v němž si pamatujeme, které objekty jsou právě v daném kontejneru.
Metoda getObject(String) vracející objekt zadaného názvu, a pokud se takový objekt v kontejneru nevyskytuje, vrátí null.
Metoda removeObject(Thing) odebírající zadaný objekt z kontejneru. Metoda tryAddObject(Thing) pokoušející se přidat do kontejneru zadaný objekt a vracející informaci o úspěšnosti tohoto pokusu. Je sice zřejmé, že do prostoru můžeme další objekt přidat vždy (tedy alespoň ve většině her), ale na druhou stranu budeme-li ochotni oželet tu nepatrnou ztrátu efektivity spojenou s testováním vložitelnosti objektu do kontejneru u prostorů, můžeme na druhou stranu ocenit, že už se o danou metodu nemusíme zabývat, protože ji zdědíme od rodiče. Navíc se takováto definice metody může hodit u her, v nichž existují prostory, jejich kapacita je (stejně jako u batohu) konečná. Otázkou je, zda bychom mohli vytknout i inicializační metody naplňující daný kontejner počáteční sadou objektů. Sice se v naší třídě prostor (Room) a batohu (Hands) jmenují rozdílně, ale mají za úkol stejnou věc. Vrátíme se k této otázce později.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 137 z 240
138
Návrh semestrálního projektu a jeho rámce – Adventura
Jak vidíte, i bez inicializačních metod je toho, co bychom mohli sdílet, docela dost. Ukazuje se, že by definice takovéhoto společného předka mohla být výhodná. Pojďme proto takovéhoto společného předka vytvořit.
8.3
Definice společného předka kontejnerů objektů
Společný předek svoji prázdnou verzi v balíčku empty_classes nemá. Nezbyde nám tedy, než jej deklarovat jako novou třídu, kterou nazveme ThingContainer, protože objekty v naší hře jsou instancemi třídy Thing. Třídu prohlásíme za potomka interfejsu IObjectContainer (jinými slovy prohlásíme, že jej třída implementuje). Třídu deklarujeme jako abstraktní, aby od ní nebylo možno vytvořit její vlastní instance a aby jako skutečné kontejnery mohli vystupovat pouze její potomci. Hned se také obrátíme na definice tříd prostorů a batohu (v naší aplikaci tříd Room a Hands) a deklarujeme v jejich hlavičkách, že tyto třídy jsou potomka právě vytvářené třídy ThingContainer. Kostru předka máme připravenu, můžeme do ní začít stěhovat jednotlivé metody a těmito metodami používané atributy.
Metoda getObjects() Přestěhujeme ze třídy Room metodu getObjects() a s ní sdruženého atributu objects. Ve třídě Hands pak stejnojmenný atribut i metodu smažeme. Po této operaci naskočí v obou třídách řada upozornění na syntaktické chyby. Pokud se podíváte na označené příkazy, společným problémem je atribut objects, s nímž všechny metody pracují. Když v rodičovské třídy odstraníte (DOČASNĚ!) z deklarace tohoto atribut modifikátor private, obě dceřiné třídy se uklidní, protože na používaný atribut opět uvidí.
Metody getObject(String) a removeObject(Thing) Budeme-li dále procházet třídou Room, tak přeskočíme metody getName() a getNeighbors(), které jsou specifické pro prostory. Přeskočíme i metodu initialize(), o níž jsem říkal, že se k ní vrátíme později, a zastavíme se u metod getObject(String) a removeObject(Thing). Obě vykonávají akce, které řeší při zvedání objektů aktuální prostor a při pokládání objektů batoh. Přestěhujeme je proto do společného rodiče.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 138 z 240
Kapitola 8: Definice společného rodiče kontejnerů objektů
139
U batohu nebudeme muset tyto metody řešit (tj. mazat), protože jsme je v něm ještě nedefinovali. Došlo by na ně až při definici příkazu pro pokládání objektů.
Zpětné nastavení atributu objects na private Tím jsme nabídku třídy Room vyčerpali, takže už nám
Metoda tryAddObject(Thing) a přesuneme se do třídy Hands. Zde na nás čeká metoda tryAddObject(Thing), která se pokusí přidat do kontejneru (zde batohu) zadaný objekt a vrátí informaci o tom, zda se jí to podařilo. Otázku, zda má smysl tuto metodu sdílet jsem rozebíral již v podkapitole 8.2 Co bychom mohli do společného předka vytknout na straně 137, kde jsem doporučil ji vytknout. Zkopírujeme ji proto do rodičovské třídy a vzápětí do ní zkopírujeme i atribut remains, který tato metoda používá. Po tomto přestěhování se ale ve třídě Hands objeví chyba, protože metoda initialize() by chtěla odstěhovaný atribut inicializovat. Pojďme se tedy zabírat inicializací.
8.4
Inicializace objektů
Aby instance obou dceřiných tříd správně fungovaly, musejí se vždy na počátku hry inicializovat. Každý z našich typů se však inicializuje trochu jinak. Musíme proto najít tu správnou míru toho, co má smysl sdílet ve společném rodiči a co necháme na každém potomku, aby si udělal po svém.
Názvy metod Odchylky začínají již u názvů metod. Metoda batohu se jmenuje klasicky, tj. initialize, kdežto stejnojmenná metoda prostor toho má na starosti více a inicializací atributu, který jsme vytkli do společného rodiče, deleguje na metodu nazvanou inicitalizeObjects(). To naštěstí není závažný problém. Můžeme do společného předka přemístit metodu initialize() definovanou ve třídě batohu a delegující příkaz ve třídě prostor změnit na super.inicitalize(). Metoda ve třídě prostor se tak stane metodou přebíjející metodu předka, takž nám NetBeans budou doporučovat, abychom její definici doplnili anotací @Override. Samozřejmě poslechneme.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 139 z 240
140
Návrh semestrálního projektu a jeho rámce – Adventura
Nastavení kapacity Přemístěním definice metody initialize() jsme ve třídě batohu zdánlivě problém vyřešili, ale opravdu jenom zdánlivě. V přestěhovaném kódu se totiž volá metoda getCapacity(), kterou rodič nezná. Musíme se tedy nějak utkat s jejím zadáváním a následným pamatováním. Jednou z možností je zavést v rodičovském konstruktoru celočíselný parametr capacity, kterou by batoh nastavoval na zadanou kapacitu a prostor na nekonečno. Nekonečno sice v dostupných hodnotách není, ale můžeme se dohodnout, že zadá-li potomek nulu, nastaví rodič kapacitu na polovinu nejvyššího možného celého čísla – Integer.MAX_VALUE. Nastavenou kapacitu si uloží do stejnojmenného konstantního atributu a bude s ním na počátku hry inicializovat atribut remains (jinými slovy v metodě initialize() se volání neexistující metody getCapacity() nahradí přiřazením hodnoty atributu capacity). se ptáte, proč jenom na polovinu, tak je to proto, abych nemusel Pokud přemýšlet nad tím, jestli už v prostoru nejsou nějaké objekty. Jinak bych musel remains zmenšit o součet jejich vah, aby jeho hodnota při odebrání objektu nepřetekla a nestala se záporným číslem. (Zkuste si, co se stane, když k největšímu celému číslu přičtete jedničku.)
Nyní už zbývá pouze upravit definice dceřiných konstruktorů. V konstruktoru místnosti přidat na počátek příkaz super(0); v konstruktoru batohu pak přidat na počátek příkaz super(CAPACITY); kde CAPACITY je statická konstanta definující kapacitu batohu (viz pasáž Metoda getCapacity() na straně 116).
Inicializace kolekce objects Metoda initialize() ale stále není dokonalá. Rozumně inicializuje pouze prázdný batoh. Pokud by v batohu měly být na počátku nějaké objekty, nastane problém a stejný problém se vyskytne u prostorů s počátečními objekty. V tuto chvíli bychom si ale mohli vzpomenout na to, že ve třídě prostor (Room) zůstala opuštěná metoda inicitalizeObjects(), která tento problém pro prostory řešila. Její tělo ale předpokládá existenci atributu obsahujícího pole s názvů objektů, které se v daném kontejneru vyskytují na počátku hry. Nahradíme inicializaci
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 140 z 240
Kapitola 8: Definice společného rodiče kontejnerů objektů
141
tributu objects v metodě initialize() příkazem převzatým z metody inicitalizeObjects() a metodu ve třídě prostor zmažeme. Problém ale ještě není vyřešen, protože použitý příkaz požaduje atribut s polem s názvy počátečních objektů. Opět budeme muset upravit konstruktor. Přidáme do něj druhý parametr, který bude obsahovat požadované názvy a který pro zjednodušení převezmeme ze třídy prostor. Konstruktor nyní bude mít signaturu ThingContainer(int capacity, String... objectNames) Do těla konstruktoru pak ze třídy prostor přesuneme příslušný inicializační příkaz. Překladači se to nebude líbit, protože příkaz používá atribut, který ještě neexistuje. Přesuneme proto ze třídy prostor i deklaraci tohoto atributu (beztak už tam nebude k ničemu). Editor se u třídy ThingContainer uklidní a přestane upozorňovat na chyby, ale ve třídě prostor a batohu hlášení zůstanou. Změnili jsme totiž signaturu rodičovského konstruktory a ony stále používají tu starou. Upravíme proto volání rodičovského konstruktoru ve třídě Room do podoby super(0, objectNames); a ve třídě Hands do podoby super(CAPACITY, new String[]{}); Nyní už se zdají být všechny tři třídy bez chyb (zdrojový kód třídy ThingContainer najdete ve výpisu 8.1) a můžeme se tedy vrhnout na definici příkazu pro pokládání objektů. Výpis 8.1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Definice třídy ThingContainer
/******************************************************************************* * Instance třídy {@code ThingContainer} představují rodičovské podobjekty * instancí, které vystupují jako kontejnery objektů, tj. prostorů a batohu. * */ abstract class ThingContainer implements IObjectContainer { //== CONSTANT CLASS ATTRIBUTES ================================================= //== VARIABLE CLASS ATTRIBUTES =================================================
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= //== OTHER NON-PRIVATE CLASS METHODS =========================================== //== PRIVATE AND AUXILIARY CLASS METHODS =======================================
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 141 z 240
142 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
Návrh semestrálního projektu a jeho rámce – Adventura
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Kapacita kontejneru. */ private final int capacity; /** Názvy objektů v místnosti na počátku hry. */ private final String[] objectNames;
//== VARIABLE INSTANCE ATTRIBUTES ============================================== /** Kolekce aktuálních objektů v daném kontejneru. */ Collection
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** * Vytvoří nový kontejner se zadanou kapacitou a názvy objektů, * které se v něm budou vyskytovat na počátku hry. * * @param capacity Kapacita kontejneru; 0 znamená neomezená * @param objectNames Názvy objektů v kontejneru na počátku hry */ ThingContainer(int capacity, String... objectNames) { this.capacity = (capacity == 0) ? Integer.MAX_VALUE/2 : capacity; this.objectNames = objectNames; }
//== ABSTRACT METHODS ========================================================== //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** * Vrátí kolekci objektů nacházejících se v daném kontejneru. * * @return Kolekce objektů nacházejících se v daném kontejneru */ @Override public Collection
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 142 z 240
Kapitola 8: Definice společného rodiče kontejnerů objektů
143
71 return Collections.unmodifiableCollection(objects); 72 } 73 74 75 /*************************************************************************** 76 * Vrátí odkaz na objekt s daným názvem uložený v osloveném kontejneru 77 * (je-li v něm více objektů se shodným názvem, vrátí libovolný z nich). 78 * 79 * @param objectName Název požadovaného objektu 80 * @return Odkaz na hledaný objekt nebo {@code null} v případě, 81 * kdy žádný objekt s takovým názvem v místnosti není 82 */ 83 Thing getObject(String objectName) 84 { 85 Thing result = INamed.get(objectName, objects).orElse(null); 86 return result; 87 } 88 89 90 /*************************************************************************** 91 * Odebere zadaný objekt ze své kolekce objektů. 92 * 93 * @param object Odebíraný objekt 94 */ 95 void removeObject(Thing object) 96 { 97 objects.remove(object); 98 } 99 100 101 /*************************************************************************** 102 * Vejde-li se zadaný objekt do batohu, přidá jej a vrátí zprávu o výsledku. 103 * 104 * @param object Objekt, který se má přidat do kontejneru 105 * @return Zpráva o výsledku: {@code true} = byl přidán, 106 * {@code false} = nebyl přidán 107 */ 108 boolean tryAddObject(Thing object) 109 { 110 if (object.getWeight() > remains) { 111 return false; 112 } 113 objects.add(object); 114 remains -= object.getWeight(); 115 return true; 116 } 117 118 119 120 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 121 122 /*************************************************************************** 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 143 z 240
144 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
Návrh semestrálního projektu a jeho rámce – Adventura * Nastaví výchozí stav kontejneru na počátku hry, * tj. zapamatuje si, kolik se do něj ještě vejde * a v atributu {@link #objects} si zapamatuje výchozí objekty. */ void initialize() { remains = capacity; objects = Arrays.stream(objectNames) .map(Thing::new) .collect(Collectors.toList()); }
//== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= }
8.5
Příkaz pro pokládání objektů – CommandPutDown
Definice tohoto příkazu je velice podobná definici příkazu CommandPickUp určeného ke zvedání objektů. Jenom tu bude trochu méně kontrol a prohodí se zdrojový a cílový kontejner, ale o tom jsem ji hovořil. Přepokládám proto, že byste dokázali naprogramovat příslušný příkaz sami. Kdo by si nebyl svým výtvorem jistý, může svůj zdrojový kód porovnat s kódem ve výpisu 8.2. Výpis 8.2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Definice metody execute(String...) ve třídě CommandPutDown
/*************************************************************************** * Přesune objekt zadaný v parametru z aktuálního prostoru do batohu. * Vyžaduje však, aby objekt byl zvednutelný a do batohu se vešel. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zOBJEKT_NEZADAN; } String objectName = arguments[1]; Room currentRoom = Apartment.getInstance().getCurrentArea(); Hands bag = Hands.getInstance();
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 144 z 240
145
Kapitola 8: Definice společného rodiče kontejnerů objektů 17 18 19 20 21 22 23 24 }
Thing object = bag.getObject(objectName); if (object == null) { return zNENÍ_V_BATOHU + objectName; } currentRoom.tryAddObject(object); bag .removeObject(object); return zPOLOŽENO + objectName;
Test Jako již pravidelně, spustíme test, abychom ověřili, jestli se nám podařilo opět o kousek posunout. Test nám ohlásí: ============================================================================= Při testu následujícího, tj. 5. kroku: Konec se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Konec hry.» Přišlo: «Tento příkaz neznám.» Takže to vypadá, že se vše podařilo a test se zastavil až při pokusu o zadání dalšího příkazu, kterým je příkaz k předčasnému ukončení hry. Pojďme jej tedy vytvořit.
8.6
Příkaz k předčasnému ukončení hry – CommandExit
Začátek již jistě znáte: zkopírovat, přejmenovat a vložit příkaz k vytvoření instance do konstruktoru správce příkazů. Můžeme se tedy vrhnout hned na úpravu metody execute(String...). I ta by ale měla být jednoduchá. Náš příkaz by měl ukončit hru a rozloučit se s hráčem. Možná si vzpomenete, že hru jsme již jednou ukončovali, konkrétně v podkapitole 6.8 Úprava informací o stavu hry na straně 109. Tehdy jsme pro správce příkazů definovali metodu stop(). Ta se nám nyní hodí, takže výsledná podoba metody bude velmi jednoduchá – viz výpis 8.3. Výpis 8.3:
Definice metody execute(String...) ve třídě CommandExit
25 /*************************************************************************** 26 * Předčasně ukončí hru. 27 * 28 * @param arguments Parametry příkazu - nepoužité 29 * @return Text zprávy vypsané po provedeni příkazu 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 145 z 240
146 30 31 32 33 34 35 36
Návrh semestrálního projektu a jeho rámce – Adventura */ @Override public String execute(String... arguments) { stop(); return zKONEC; }
Test Po spuštění pravidelného testu ověřujícího správnost doposud navržené části programu se dozvíme, že testovací program je nyní spokojen a ukončí proto výpis zprávou: ===== Konec testu prováděných operací ===== ############################################################################# ##### Čas: Sun Dec 14 14:01:28 CET 2014 ##### Hra: org.edupgm.adventure.common.rup14p.OfficialApartmentGame ##### Autor: RUP14P - PECINOVSKÝ Rudolf ##### Scénář: REQUIRED ############################################################################# Podle výsledků testu program pravděpodobně neobsahuje žádné závažné chyby #############################################################################
8.7
Prověření programu chybovým scénářem
Pokud ve scénáři testujícím chybná zadání uživatele nemáte nestandardní příkazy, měl by nyní projít i ten. Tento scénář navíc prověří, že jsme při návrhu mysleli na všechny krizové eventuality a vyřešili je. Otevřeme proto soubor správce scénářů a v jeho metodě main(String[]) upravíme příkaz MANAGER.testGameByScenario(REQUIRED_STEPS_SCENARIO_NAME); na MANAGER.testGameByScenario(MISTAKE_SCENARIO_NAME); a program znovu spustíme. Tentokrát bude test ukončen zprávou:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 146 z 240
147
Kapitola 8: Definice společného rodiče kontejnerů objektů ============================================================================= Při testu následujícího, tj. 2. kroku: se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Zadal(a) jste prázdný příkaz.» Přišlo: «Tento příkaz neznám.»
Doplnění reakce na prázdný příkaz Zpráva nám oznamuje, že jsme špatně ošetřili zadání prázdného příkazu při běhu hry. Otevřeme proto ve třídě ACommand (třída správce příkazů) metodu execute(String), která rozhoduje o tom, který objekt bude pověřen provedením zadaného příkazu (viz výpis 5.6 na straně 90, řádky 61 až 72). Z její definice se dozvíme, že u hry, která neběží, řeší reakci na prázdný příkaz metoda startGame(String), ale u hry, která běží, řeší veškeré situace konkrétní příkazy. Prázdný příkaz jsme ještě nedefinovali, a proto jej správce příkazů nezná. Situaci můžeme řešit dvěma způsoby:
Definujeme prázdný příkaz. Rozhodneme se, že reakce na prázdný příkaz je tak jednoduchá, že kvůli ní není třeba definovat zvláštní příkaz, a že bude výhodnější ji rovnou zapracovat do této metody. Mně je sympatičtější druhá varianta, a proto se vydám touto cestou a upravím metodu do tvaru, který si můžete prohlédnout ve výpisu 8.4. Výpis 8.4:
Definice metody executeCommand(String) ve třídě ACommand
1 static String executeCommand(String command) 2 { 3 command = command.trim(); 4 String answer; 5 if (isAlive) { 6 if (command.isEmpty()) { 7 answer = zPRÁZDNÝ_PŘÍKAZ; 8 } 9 else { 10 answer = executeCommonComand(command); 11 } 12 } 13 else { 14 answer = startGame(command); 15 } 16 return answer; 17 }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 147 z 240
148
Návrh semestrálního projektu a jeho rámce – Adventura
Test Spustíme obligátní test a ten nám oznámí: ===== Konec testu prováděných operací ===== ############################################################################# ##### Čas: Sun Dec 14 14:57:24 CET 2014 ##### Hra: org.edupgm.adventure.common.rup14p.OfficialApartmentGame ##### Autor: RUP14P - PECINOVSKÝ Rudolf ##### Scénář: _MISTAKE_ ############################################################################# Podle výsledků testu program pravděpodobně neobsahuje žádné závažné chyby ############################################################################# Se standardními příkazy jsme tedy hotovi a můžeme se pustit do těch nestandardních. To ale odložím do příští kapitoly.
8.8
Co jsme prozatím naprogramovali
Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 7.8 Co jsme prozatím naprogramovali na straně 134, jsme přidali následující:
Definovali
jsme třídu ThingContainer jako společného rodiče tříd, jejichž instance obsahují kolekce objektů, tj. třídy prostorů (v našem příkladu Room) a třídy batohu (v našem projektu Hands). Do této třídy jsme vytkli metody, které využijeme jak při zvedání objektů, tak při jejich pokládání, pouze se v nich prohodí zdroj a cíl.
Definovali jsme třídu CommandPutDown, jejíž instance je zodpovědná za správnou reakci na příkaz odebrání objektu z batohu a jeho přesun do aktuálního prostoru.
Definovali
jsme třídu CommandExit, jejíž instance je zodpovědná za správnou reakci na příkaz k ukončení hry.
Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 8.1.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 148 z 240
Kapitola 8: Definice společného rodiče kontejnerů objektů
149
Obrázek 8.1 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující všechny povinné příkazy hry
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A08z_ObjectContainer.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 149 z 240
150 9.
Návrh semestrálního projektu a jeho rámce – Adventura Definice nestandardních příkazů
Kapitola 9 Definice nestandardních příkazů
9.1
Co se v kapitole naučíme V předchozích kapitolách jsme postupně definovali třídy reprezentující jednotlivé součástí světa hry a po nich třídy, jejichž instance reprezentují jednotlivé povinné příkazy. V této kapitole se pustíme do definice těch zbývajících příkazů. Tyto příkazy jsou ve scénářích uváděny jako nestandardní a v každé hře mohou být jiné. Proto se budu v textu snažit ukázat spíše principy, které byste mohli využít při definici nestandardních příkazů ve vaší hře.
Změna testu
V minulých kapitolách jsme pracovali nejprve s jednoúčelovým scénářem pro prověření funkce povinných příkazů, a pak s chybovým scénářem prověřujícím jejich funkce v situacích, kdy hráč tyto příkazy zadá chybně. Ti, jejichž chybové scénáře neobsahují nestandardní příkazy, by již měli mít definice všech standardních příkazů kompletně prověřené. Máme-li definovat nestandardní příkazy a prověřit jejich funkci, musíme použít úspěšný scénář, protože v něm mají být definovány reakce oněch nestandardních příkazů. Vrátíme se proto k původnímu testu hry, v němž se nejprve prověřuje funkčnost hry podle úspěšného scénáře a následně pak podle chybového scénáře. Při něm nám test oznámí:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 150 z 240
151
Kapitola 9: Definice nestandardních příkazů ============================================================================= Při testu následujícího, tj. 7. kroku: Otevři Lednička se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Lednička nejde otevřít. Na ledničce leží nějaký popsaný papír.» Přišlo: «Tento příkaz neznám.»
9.2
Definice příkazu pro otevření ledničky
Příkaz pro otevření ledničky je poněkud komplikovanější, protože lednička má jít otevřít pouze v případě, kdy bude něčím podložena. Musíme tedy někde uchovávat informaci o tom, zda hráč již ledničku podložil. Vzhledem k tomu, že k podložení ledničky je třeba zadat jiný příkaz, musí být daná informace dostupná oběma příkazům: jeden ji nastavuje a druhý zjišťuje. Máme několik možností, jak takovou informaci uchovávat:
Požadovanou
informaci by mohla uchovávat sama lednička – podkládající příkaz by ji nastavil a otevírací otestoval. Nevýhodou tohoto řešení je to, že bychom pro ledničku museli definovat zvláštní třídu, která by byla potomkem třídy Room, protože lednička vlastně představuje zvláštní druh prostoru, v němž se mohou vyskytovat objekty. Jeho zvláštností je, že nemá žádné sousedy a „vchází“ se do něj nestandardním příkazem CommandOpen.
Pokud by se nám nechtělo definovat pro ledničku zvláštní třídu, mohli bychom přidat další kategorii objektů – objekty, které je možno otevřít. Definovali bychom pro ně zvláštní příznak – např. "O" a z objektů v naší hře bychom sem mohli zařadit vedle ledničky třeba také deštník. Kdybychom chtěli opravdu vymyslet hru s řadou možností, mohli bychom se touto cestou vydat. Každý předmět by pak musel umět prozradit, jestli je možno jej otevřít a ty otevřitelné by si navíc museli pamatovat, jestli aktuálně jsou nebo nejsou otevřeny. Naším primárním cílem však není vymyslet co nejrafinovanější hru, ale naučit se vyvíjet trochu rozsáhlejší aplikace, než jsou ty, které jsme v úvodním kurzu vytvářeli doposud. Proto tuto cestu zamítneme jako zbytečně složitou.
Další
možností, jak vyřešit náš problém, je ukládat informace o stavu hry např. ve správci příkazů. K němu mají všechny příkazy přístup, protože je jejich společným rodičem. Jeden příkaz by u něj informaci ukládal a jiný zjišťoval.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 151 z 240
152
Návrh semestrálního projektu a jeho rámce – Adventura Nevýhodou tohoto řešení je, že na správce příkazů nakládáme další zodpovědnost, čímž narušujeme pravidlo, že každý objekt se má starat pouze o jedinou věc. Nakládání více zodpovědností na jeden objekt může vést v budoucnu k chybám zapříčiněných právě přehlédnutím důsledků zanesených oprav na alternativní aktivity daného objektu.
Další možností, kam ukládat příznaky o stavu hry, je správce světa hry. Ten by mohl v rámci své starosti o svět hry udržovat i některé pomocné informace. V našem příkladu by to byly např. informace o tom, je-li lednička podložena nebo má-li hráč nasazeny brýle. O této možnosti by už mělo smysl uvažovat, protože bychom na správce světa hry nenakládali zcela novou zodpovědnost, ale pouze bychom trochu rozšířili jeho zodpovědnosti stávající.
Mohli bychom ale také definovat zvláštní třídu určenou jenom k tomu, aby její objekt uchovával všechny příznaky. Třída by nepotřebovala žádné instance, protože by se o vše mohl postarat objekt třídy. Já bych se přikláněl k poslednímu uvedenému řešení, protože mi připadá pro programátora nejpřehlednější. Definujme proto knihovní třídu Flags se statickými atributy a jejich přístupovými metodami. Třída i její metody by měly mít přístupová práva package private, protože nikdo jiný, kromě tříd v daném balíčku o nich nemusí vědět. Až se příště objeví potřeba uchovávat hodnoty nějakých dalších příznaků, budeme dopředu vědět, kde definovat příslušné atributy a jejich přístupové metody.
9.3
Definice třídy Flags
Třídu Flags vytvoříme standardním způsobem. Obsahuje (přesněji bude obsahovat) totiž kód, který je specifický pro danou hru, a nemá proto v balíčku empty_classes svoji předpřipravenou verzi. Její definici nám může usnadnit to, že stačí pouze definovat potřebný atribut a o definici příslušných přístupových metod pak můžeme požádat vývojové prostředí, které je doplní za nás. Na nás zbyde už jen doplnění dokumentačních komentářů. Při zavádění nového příznaku by nás mohlo napadnout, že bychom jej měli na počátku každé hry uvést do správného počátečního stavu. Měli bychom proto ve třídě definovat metodu initialize(), která bude mít veškeré inicializace na starosti a jejíž volání přidáme mezi jednotlivé inicializační příkazy ve stejnojmenné metodě správce příkazů – v našem případě mezi příkazy metody ACommand.initialize()
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 152 z 240
153
Kapitola 9: Definice nestandardních příkazů
– její upravený kód si můžete prohlédnout ve výpisu 9.1. Kód dosavadní verze třídy Flags pak najdete ve výpisu 9.2. Výpis 9.1: 1 2 3 4 5 6 7 8 9 10
/*************************************************************************** * Postupně nechává inicializovat všechny objekty, * jejichž stav se mohl od počátku hry změnit. */ static void initialize() { Apartment.getInstance().initialize(); Hands .getInstance().initialize(); Flags .initialize(); }
Výpis 9.2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
Upravená definice statické metody initialize() třídy ACommand
Předběžné podoba definice třídy Flags
/******************************************************************************* * Knihovní třída {@code Flags} je schránkou na nejrůznější příznaky, * které si je potřeba v průběhu hry zapamatovat. */ class Flags { //== CONSTANT CLASS ATTRIBUTES ================================================= //== VARIABLE CLASS ATTRIBUTES ================================================= /** Příznak registrující, zda již byla lednička podložena. */ private static boolean iceboxSupported;
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= /*************************************************************************** * Vrátí informaci o tom, je-li lednička podložena. * * @return Je-li podložena, vrátí {@code true}, jinak vrátí {@code false} */ static boolean isIceboxSupported() { return iceboxSupported; } /*************************************************************************** * Nastaví informaci o tom, je-li lednička podložena. * * @param iceboxSupported Nastavovaná hodnota */
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 153 z 240
154 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
Návrh semestrálního projektu a jeho rámce – Adventura static void setIceboxSupported(boolean iceboxSupported) { Flags.iceboxSupported = iceboxSupported; }
//== OTHER NON-PRIVATE CLASS METHODS =========================================== /*************************************************************************** * Nastaví počáteční hodnoty všech příznaků. */ static void initialize() { iceboxSupported = false; }
//== PRIVATE AND AUXILIARY CLASS METHODS =======================================
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== //== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /** Soukromý konstruktor zabraňující vytvoření instance. */ private Flags() {}
//== //== //== //==
ABSTRACT METHODS ========================================================== INSTANCE GETTERS AND SETTERS ============================================== OTHER NON-PRIVATE INSTANCE METHODS ======================================== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 154 z 240
155
Kapitola 9: Definice nestandardních příkazů
9.4
Vytvoření třídy CommandOpen
Vše potřebné již máme připraveno, a můžeme proto definovat třídu CommandOpen, jejíž instance bude zodpovědná za správnou reakci na příkaz pro otevření ledničky. Pro jistotu připomenu standardní začátek definice třídy příkazu: zkopírovat (nejlépe třídu CommandMove, protože otevírání ledničky je zvláštní případ přesunu do jiného prostoru), přejmenovat na CommandOpen, upravit definici konstruktoru a přidat jeho volání do konstruktoru správce příkazů, tj. do statického konstruktoru třídy ACommand.
9.5
Definice metody execute(String...)
Takže „formality“ máme za sebou a můžeme se vrhnout na to hlavní, na úpravu těla metody execute(String...), které má v současnosti podobu zobrazenou ve výpisu 7.9 na straně 129 (upravovaná třída CommandOpen vznikla jako kopie třídy CommandMove, a metoda má proto zatím její verzi těla). Pojďme si je prohlédnout a zamysleme se nad tím, jak je upravit.
Test existence parametru Na počátku (ve výpisu 7.9 na řádcích 12 až 14) metoda prověřuje, zda bylo vůbec zadáno, kam se má přejít. V naší metodě bychom testovali, co se má otevřít. Tento test bychom měli určitě ponechat. Vracený text se nám ale nehodí, takže bychom jej měli upravit. Protože však pro definici textů používáme konstanty, měli bychom ve třídě Texts definovat novou konstantu – vložíme ji na začátek sekce se zprávami definujícími reakce na zadání nestandardních příkazů (ve výpisu 3.5 na straně 56 začíná tato sekce na řádku 112): /** Texty zpráv vypisované v reakci na nepovinné příkazy. */ static final String zOTEVÍRANÝ_OBJEKT_NEZADÁN = zANP + "\nNebylo zadáno, co se má otevřít", zLEDNICE_NEJDE_OTEVŘÍT = "\nLednička nejde otevřít. Na ledničce leží nějaký popsaný papír.", Nyní můžeme zaměnit původní konstantu za konstantu se zprávou oznamující nezadání objektu, který se má otevřít. Další příkaz ukládající název otevíraného objektu do lokální proměnné destinationName (ve výpisu 7.9 příkaz na řádku 15) ponecháme. S tímto názvem ještě budeme potřebovat pracovat. Jenom změníme název proměnné na objectName. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 155 z 240
156
Návrh semestrálního projektu a jeho rámce – Adventura
Test přítomnost otevíraného objektu Příkaz na řádku 16 zjišťuje aktuální prostor, aby jej pak další příkaz (řádky 17 a 18) mohl požádat o sousední prostor se zadaným názvem. My se sice na sousední prostory prát nemusíme, ale bylo by vhodné, kdybychom ověřili, že se otevíraný objekt v daném prostoru vůbec nachází. Jinak by mohl hráč otevřít ledničku třeba již z předsíně. Upravíme proto příkaz tak, abychom se aktuálního prostoru neptali na sousedy, ale na přítomnost objektu, který máme otevřít. Příkaz proto bude mít tvar: Thing object = currentRoom.getObject(objectName); Nyní se můžeme zeptat, zda se objekt v daném prostoru nachází, a pokud tomu tak není, vrátíme stejnou zprávu, jakou jsme vraceli, když v prostoru nebyl objekt, který jsme měli zvednout.
Test otevíratelnosti zadaného objektu Dobrá, víme tedy, že se objekt v prostoru nachází. Měli bychom ještě ověřit, že daný objekt je možno otevřít, aby se nestalo, že by uživatel chtěl otevřít třeba papír. Opět vyvstává otázka, jak rozpoznat objekty, které je možno otevřít a opět máme několik možností:
Rozšířit definici objektů tak, aby každý z nich věděl, jestli je možno jej otevřít. Definovat někde (např. ve třídě Flags) seznam objektů, které je možno otevřít. Najít nějakou charakteristiku, podle níž bychom to poznali hned, aniž bychom museli rozšiřovat definice stávajících tříd. Mně se opět nejvíc líbí poslední možnost. Za prvé proto, že vyžaduje minimální úpravu stávajícího kódu a za druhé proto, že takovou vlastnost už naše lednička opravdu má. Lednička je totiž současně objektem v prostoru a současně prostorem. Můžeme si ji tedy vyžádat od správce všech prostorů (přesněji od správce světa hry). Vrátí-li null, oznámíme uživateli, že zadaný prosto není možno otevřít (budeme kvůli tomu muset definovat další statickou konstantu ve třídě Texts): zNENÍ_OTEVÍRATELNÝ = zANP + "\nZadaný objekt není otevíratelný: ",
Dokončení definice Vše, co bylo třeba před spuštěním příkazu prověřit, jsme prověřili (alespoň si to myslíme), takže můžeme definici metody execute(String...) dokončit. Požádáme
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 156 z 240
157
Kapitola 9: Definice nestandardních příkazů
správce světa hry o to, aby nastavil požadovaný cílový prostor, a vrátíme požadovanou výstupní zprávu. Po spuštění testu se dozvíme: ============================================================================= Při testu následujícího, tj. 7. kroku: Otevři Lednička se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Lednička nejde otevřít. Na ledničce leží nějaký popsaný papír.» Přišlo: «Přesunul(a) jste se do místnosti: Lednička»
Doplnění testu podloženost Ajta! Zapomněli jsme na to, že ledničku je možno otevřít pouze poté, co ji nějakým vhodným předmětem podložíme. Před příkaz pro vlastní přesun do požadovaného prostoru proto musíme ještě vložit test podloženosti daného objektu. Zavoláme proto metodu Flags.isIceboxSupported() a upravíme vracení zprávy podle jejího výsledku. Definici metodu upravené podle výše zmíněných zásad si můžete prohlédnout ve výpisu 9.3. Výpis 9.3: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
Definice metody execute(String...) ve třídě CommandOpen
/*************************************************************************** * Otevře zadaný objekt a přesune do něj hráče jako do nového prostoru. * Vyžaduje však, aby objekt byl opravdu otevřitelný, * tj. aby existoval prostor pojmenovaný stejně jako daný objekt. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zOTEVÍRANÝ_OBJEKT_NEZADÁN; } String objectName = arguments[1]; Room currentRoom = Apartment.getInstance().getCurrentArea(); Thing object = currentRoom.getObject(objectName); if (object == null) { return zNENÍ_OBJEKT + objectName; } Room destinationArea = Apartment.getInstance().getRoom(objectName); if (destinationArea == null) { return zNEJDE_OTEVŘÍT + objectName; } if (Flags.isIceboxSupported()) { Apartment.getInstance().setCurrentArea(destinationArea);
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 157 z 240
158 27 28 29 30 31 32 }
Návrh semestrálního projektu a jeho rámce – Adventura return OTEVŘEL_LEDNIČKU; } else { return LEDNICE_NEJDE_OTEVŘÍT; }
Test Po spuštění testu krok testující právě definovaný příkaz úspěšně projde a testovací program se zastaví se zprávou: ============================================================================= Při testu následujícího, tj. 10. kroku: Vezmi Papír se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Vzal(a) jste objekt: Papír» Přišlo: « Zadaná akce nebyla provedena Zadaný objekt nemůžete vzít, máte už obě ruce plné»
9.6
Oprava definice příkazu pro položení objektu
Podíváme-li se zpět na výpisu průběhu předchozích příkazů, zjistíme, že v předchozím korku jsme položili časopis, takže nemůžeme mít obě ruce plné. V příkazu pro položení předmětu je zřejmě chyba – příkaz nejspíš neaktualizuje volný prostor v batohu. Otevřeme tedy znovu zdrojový kód třídy CommandPutDown a najdeme v něm definici metody execute(String...) (její dosavadní definici najdete ve výpisu 8.2 na straně 144). V této definici bude pro nás zřejmě klíčový příkaz odebírající objekt z batohu (ve výpisu 8.2 je na řádku 22): bag.removeObject(object); Klepneme-li na název metody při stisknutém přeřaďovači CTRL, přesuneme se na její definice ve třídě ThingContainer (najdete ji ve výpisu 8.1 na straně 141 na řádcích 90 až 98). Tato definice odebere zadaný objekt z kolekce objects. V tom problém není (kdyby byl, tak by si testovací program již dávno stěžoval na to, že mu nesedí očekávané a obdržené objekty v batohu). Problém je v tom, že si program stále myslí, že batoh je plný. Podíváte-li se na definici následující metody tryAddObject(Thing), zjistíte, že tato metoda předposledním příkazem remains -= object.getWeight(); 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 158 z 240
159
Kapitola 9: Definice nestandardních příkazů
(ve výpisu 8.1 na řádku 114) zmenšuje zbývající objem batohu. Metoda pro odebírání objektu z batohu by jej proto měla naopak zvětšit. To ale nedělá. Přidáme do ní proto příkaz: remains += object.getWeight(); a spustíme znovu test. Tentokrát již příkaz projde a test skončí s hlášením: ============================================================================= Při testu následujícího, tj. 11. kroku: Přečti Papír se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Rozhodl(a) jste se přečíst vzkaz na papíře.» Přišlo: « Tento příkaz neznám. Jdeme tedy vytvářet třídu, jejíž instance bude mít na starosti reakci na příkaz přečti.
9.7
Definice příkazu pro přečtení papíru
Nejprve si připomeneme, k čemu má tento příkaz sloužit. Při nezdařeném pokusu o otevření lednice se hráč dozví, že na lednici leží papír. Očekává se, že jej vezme a přečte, aby se dozvěděl, co dál. Má-li papír přečíst, musí jej mít v ruce (tj. v „batohu“). Navíc musí mít nasazené brýle, protože jinak pro něj bude text nečitelný. Začneme standardně: zkopírujeme třídu CommandOpen, pojmenujeme vytvořenou kopii CommandRead, upravíme definici konstruktoru a přidáme jeho volání do konstruktoru správce příkazů, tj. do statického konstruktoru třídy ACommand. Poté přejdeme na definici metody execute(String...). Vyjdeme z definice ve výpisu 9.3 na straně 157, který postupně upravíme do požadované podoby.
Test existence parametru Začneme testem existence parametru (výpisu 9.3 na řádcích 12 až 14). Test měnit nemusíme, pouze musíme změnit vracený text. Přesuneme se proto do třídy Texts a na konec sady zpráv o nezadaných parametrech přidáme definici: zNEZADÁNO_CO_ČÍST = zANP + "\nNebylo zadáno, co se má přečíst", Proměnnou zNEZADÁNO_CO_ČÍST pak zadáme jako návratovou hodnotu naší metody v případě, že nebyl zadán parametr.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 159 z 240
160
Návrh semestrálního projektu a jeho rámce – Adventura
Test přítomnosti daného objektu v batohu Nyní bychom podle zadání měli otestovat, zda je objekt, který se má přečíst, přítomen batohu. Čtení jiných objektů hra nepovoluje. Požádáme proto batoh o odkaz na ten ze svých objektů, jehož název odpovídá názvu zadanému v parametru.
Vrátí-li prázdný odkaz, oznámíme hráči, že objekt, který chce číst, není v daném okamžiku v batohu.
Vrátí-li odkaz na objekt, můžeme pokračovat. Test, zda je požadováno přečtení objektu, který lze opravdu přečíst Dalším požadavkem je, aby uživatel požadoval přečtení papíru. Z objektů dostupných v našem světě bychom teoreticky mohli povolit i čtení časopisu. Můžeme si pro zajímavost ukázat, jak zpracovat čtení obou objektů. Je zřejmé, že musíme nejprve zjistit, jestli chce hráč přečíst objekt, který se přečíst dá. To ale bohužel nestačí, protože k přečtení objektu musí mít hráč nasazeny brýle. Nasazení brýlí je však vhodné prověřovat až poté, kdy víme, že objekt lze číst, protože ve scénáři odpověď při nenasazených brýlích říká, že písmena jsou příliš malá a rozmazaná. Potřebujeme se proto rozhodnout, jak vyřešit zařazení stejné akce (upozornění na brýle) do dvou rozdílných větví (papír versus časopis). Máme několik možností:
Test na nasazení brýlí přidáme do obou větví. Bude se sice opakovat stejný kód, ale nebudeme muset dvakrát testovat objekt určený k přečtení.
Otestujeme nejprve, zda se jedné o některý z objektů, které lze přečíst, pak prověříme nasazení brýlí a poté vrátíme zprávu podle toho, který objekt máme číst.
Při testu čtitelnosti objektu si hned zapamatujeme případnou odpověď, poté otestujeme nasazení brýlí, a pokud budou nasazeny, vrátíme předem zapamatovanou dopověď. Já bych se přikláněl k poslednímu uvedenému řešení.
Test nasazených brýlí S nasazením brýlí je to obdobné, jako s podložením ledničky: musíme si zapamatovat aktuální stav, aby nevadilo, že mezi nasazením a použitím bylo vykonáno
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 160 z 240
161
Kapitola 9: Definice nestandardních příkazů
několik dalších akcí. Hráč si je mohl nasadit hned poté, co je našel, a pak ještě chvíli bloumat po bytě. Při úvahách o podložení ledničky jsme definovali třídu Flags. To je to správné místo, kam bychom nyní mohli umístit i příznak toho, zda již má hráč nasazeny brýle. Definujeme v ní proto atribut glassesPutOn spolu s příslušnými přístupovými metodami. Současně nezapomeneme přidat inicializaci tohoto příznaku do metody initialize(), aby na počátku příštího spuštění hry byl hráč opět bez brýlí.
Výsledná podoba metody Výslednou podobu těla metody navržené podle předchozích úvah si můžete prohlédnout ve výpisu 9.4. Výpis 9.4: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
Definice metody execute(String...) ve třídě CommandRead
/*************************************************************************** * Simuluje čtení objektu, který má hráč v batohu a vrátí text, * který má být na daném objektu napsán. * Vyžaduje, aby se čtený objekt jmenoval buď papír * nebo časopis, byl v okamžiku zadání příkazu přítomen v batohu * a aby měl hráč nasazeny brýle. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zNEZADÁNO_CO_ČÍST; } String objectName = arguments[1]; Thing object = Hands.getInstance().getObject(objectName); if (object == null) { return zNENÍ_V_BATOHU + objectName; } String message; if (PAPÍR.equalsIgnoreCase(objectName)) { message = zNAPSÁNO_PAPÍR; } else if (ČASOPIS.equalsIgnoreCase(objectName)) { message = zNAPSÁNO_ČASOPIS; } else { return zNEČTITELNÝ_OBJEKT + objectName; } return zCHCE_PŘEČÍST + object.getName() +
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 161 z 240
162
Návrh semestrálního projektu a jeho rámce – Adventura (Flags.isGlassesPutOn() ? message : zNEMÁ_BRÝLE);
34 35 }
Test Po dokončení metody spustíme pravidelný test, který nám prozradí: ============================================================================= Při testu následujícího, tj. 12. kroku: Nasaď Brýle se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Nasadil(a) jste si brýle.» Přišlo: « Tento příkaz neznám. Naše definice tedy prozatím prošla a můžeme se vrhnout na další příkaz.
9.8
Definice příkazu pro nasazení brýlí
Definice třídy CommandPutOnGlasses, jejíž instance budou mít na starosti zpracování příkazu pro nasazení brýlí, nepřináší žádný nový problém. Předpokládám, že byste ji byli jistě schopni definovat sami. Výpis 9.5 s definicí metody execute(String...) proto uvádím pouze pro kontrolu. se někdo diví tomu, že jsem tentokrát dal do názvu třídy nejenom Pokud název příkazu, ale i požadovanou hodnotu parametru, tak je to proto, že tento příkaz s jinou hodnotou parametru nepracuje (přesněji ohlásí chybu).
Výpis 9.5: 1 2 3 4 5 6 7 8 9 10 11 12 13
Definice metody execute(String...) ve třídě CommandPutOnGlasses
/*************************************************************************** * Simuluje nasazení zadaného objektu, přičemž tímto objektem musí být * brýle, které navíc musí mít hráč v daném okamžiku v batohu. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zNEZADÁNO_CO_NASADIT; }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 162 z 240
163
Kapitola 9: Definice nestandardních příkazů 14 15 16 17 18 19 20 21 22 23 24 25 26 27 }
String objectName = arguments[1]; Thing glasses = Hands.getInstance().getObject(objectName); if (glasses == null) { return zNENÍ_V_BATOHU + objectName; } if (BRÝLE.equalsIgnoreCase(objectName)) { Flags.setGlassesPutOn(true); Hands.getInstance().removeObject(glasses); return zNASADIL_BRÝLE; } else { return zNELZE_NASADIT + objectName; }
Test Po pravidelném testování se dozvíme: ============================================================================= Při testu následujícího, tj. 15. kroku: Podlož Lednička Časopis se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Rozhodl(a) jste se podložit objekt Lednička objektem Časopis.» Přišlo: « Tento příkaz neznám. Vypadá to tedy, že naše definice prošla a jdeme se vrhnout na definici třídy, jejíž instance budou zodpovědné za reakci na příkaz podlož.
9.9
Definice příkazu pro podložení ledničky
Před definicí nové třídy, kterou bychom mohli pojmenovat CommandSupportIcebox, se opět zamyslíme, jaké chování scénář pravděpodobně vyžaduje a jaké bychom měli ještě přidat z logiky věci. Naše úvahy by nás měly dovést k následujícím požadavkům:
Musí být zadány dva parametry: První definuje, co se má podložit. Druhý definuje, čím se má zmíněný objekt podložit.
Podkládaný objekt se musí vyskytovat v aktuálním prostoru. Podkládací objekt se musí vyskytovat v batohu.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 163 z 240
164
Návrh semestrálního projektu a jeho rámce – Adventura
Podložit smíme pouze vybranými objekty. Z objektů v naší hře je pro tento účel vhodný pouze časopis. Je zřejmé, že podkládat ledničku deštníkem, županem nebo brýlemi asi není dobrý nápad.
Aby
mohl hráč podkládaný objekt nadzdvihnout, musí mít nejméně jednu ruku volnou, tj. batoh nesmí být plný.
Test plnosti batohu První tři body jsme již řešili, takže byste si s nimi měli umět poradit. Jediným doposud neřešeným problémem je požadavek na „neplnost“ batohu. Tady si opět můžeme vybrat z několika možných řešení:
Zkusíme do batohu vložit fiktivní předmět (nazveme jej např. Díra). Nepodaří-li se objekt do batohu vložit, víme, že batoh je plný a vrátíme příslušnou chybovou zprávu.
Nepodaří-li se objekt do batohu vložit, tak jej opět vyjmeme a můžeme se pokusit zadaný objekt podložit.
Naučíme batoh prozradit, zda je v něm ještě volno (přidáme mu odpovídající metodu). Druhé řešení působí čistším dojmem, i když třída batohu (u nás třída Hands) nebude pro definici této metody zrovna ta pravá, protože jsme z ní většinu metod vytkli do rodičovské třídy, která jediná ví, zda se do daného kontejneru ještě něco vejde. Doplníme tedy třídu ThingContainer o metodu isFull(), která prozradí, zda je kontejner plný. Její definici byste jistě zvládli sami, ale pro úplnost ji uvádím ve výpisu 9.6. Výpis 9.6: 1 2 3 4 5 6 7 8 9 10
Definice metody isFull() instancí třídy ThingContainer
/*************************************************************************** * Vrátí informaci o tom, zda je kontejner již plný, * anebo zda se do něj ještě něco vejde. * * @return Je-li plný, vrátí {@code true}, není-li, vrátí {@code false} */ boolean isFull() { return remains == 0; }
Teoreticky bychom samozřejmě mohli doplnit univerzálnější metodu nazvanou např. getReamins(), která by prozradila, kolik je v kontejneru ještě volného místa, ale protože takovouto dokonalou informaci zatím nepotřebujeme, zůstaneme u jednodušší metody idFull(), s níž se nám bude i jednodušeji pracovat. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 164 z 240
165
Kapitola 9: Definice nestandardních příkazů
Definice metody execute(String...) Se vším, co pro definici této metody potřebujeme, jsme se již setkali, anebo jsme si to již připravili. Můžeme se tedy směle pustit do definice výkonné metody execute(String...). Protože byste již měli mít všechny potřebné vědomosti, doporučuji vám si zkusit tuto metodu naprogramovat sami a na výpis 9.7, v němž je metoda definovaná, se následně podívat pouze pro kontrolu. Výpis 9.7: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Definice metody execute(String...) instancí třídy CommandSupportIcebox
/*************************************************************************** * Simuluje čtení objektu, který má hráč v batohu a vrátí text, * který má být na daném objektu napsán. * Vyžaduje, aby se čtený objekt jmenoval buď papír * nebo časopis, byl v okamžiku zadání příkazu přítomen v batohu * a aby měl hráč nasazeny brýle. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zNEZADÁNO_CO_PODLOŽIT; } if (arguments.length < 3) { return zNEZADÁNO_ČÍM_PODLOŽIT; } String supportedName = arguments[1]; String supportingName = arguments[2]; String message1 = zCHCE_PODLOŽIT + supportedName + zOBJEKTEM + supportingName + '.'; String messageErr = message1 + zANP; Room currentRoom = Apartment.getInstance().getCurrentArea(); Thing supportedObject = currentRoom.getObject(supportedName); if (supportedObject == null) { return messageErr + zPODKLÁDANÝ_NENÍ_V_MÍSTNOSTI + supportedName; } Hands hands = Hands.getInstance(); Thing supportingObject = hands.getObject(supportingName); if (supportingObject == null) { return messageErr + zPODKLÁDACÍ_NENÍ_V_BATOHU + supportedName; } if (! LEDNIČKA.equalsIgnoreCase(supportedName)) { return messageErr + zTOTO_NELZE_PODLOŽIT + supportedName;
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 165 z 240
166 40 41 42 43 44 45 46 47 48 49 50 51 52 }
Návrh semestrálního projektu a jeho rámce – Adventura } else if (ČASOPIS.equalsIgnoreCase(supportedName)) { return messageErr + zTÍMTO_NELZE_PODLOŽIT + supportingName; } else if (hands.isFull()) { return messageErr + zNELZE_NADZVEDNOUT; } else { hands.removeObject(supportingObject); Flags.setIceboxSupported(true); return message1 + zLEDNIČKA_PODLOŽENA; }
Jak vidíte, metoda je nechutně dlouhá. Měli bychom s tím něco udělat. Teď ale v prvním kole nejprve aplikaci rozchodíme, a poté můžeme začít vylepšovat její architekturu. Věnujeme tomu samostatnou kapitolu.
Test Test spuštěný po dokončení definice třídy CommandSupportIcebox popojede zase o kousek dále a zastaví se s hlášením: ============================================================================= Při testu následujícího, tj. 19. kroku: Vezmi Pivo se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Pokoušíte si vzít z inteligentní ledničky Pivo.» Přišlo: « Vzal(a) jste objekt: pivo» Podařilo se tedy ledničku podložit a následně otevřít tak, aby test byl spokojen. Zastavil se až u braní piva, protože lednička se tváří, že je inteligentní, a je proto vydávat alkoholické nápoje pouze zletilým osobám. O tom, jak naprogramovat objekty, které se s vámi snaží zapřádat rozhovor, si povíme v příští kapitole.
9.10 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 8.8 Co jsme prozatím naprogramovali na straně 148, jsme přidali následující:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 166 z 240
167
Kapitola 9: Definice nestandardních příkazů
Definovali
jsme třídu Flags jako schránku na nejrůznější příznaky, které je třeba si pamatovat v průběhu hry. Každý příznak je definovaný jako statický atribut s příslušnými přístupovými metodami.
Definovali
jsme třídu CommandOpen, jejíž instance je zodpovědná za správnou reakci na příkaz pro otevření ledničky.
Opravili jsme chybu v metodě execute(String...) instancí třídy CommandPutDown. Definovali jsme třídu CommandRead, jejíž instance je zodpovědná za správnou reakci na příkaz k přečtení zadaného objektu – papíru nebo časopisu.
Definovali
jsme třídu CommandSupportIcebox, jejíž instance je zodpovědná za správnou reakci na příkaz pro podložení ledničky.
Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 9.1.
Obrázek 9.1 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující nestandardní příkazy hry bez podpory rozhovoru
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A09z_NonstandardCommands.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 167 z 240
168
Návrh semestrálního projektu a jeho rámce – Adventura
9.11 Co jsme prozatím přeskočili Při výkladu definice tříd, jejichž instance budou zodpovědné za zpracování jednotlivých nestandardních příkazů, jsem v zájmu stručnosti vynechal jednu důležitou věc, kterou jsou testy. V chybovém scénáři jste měli za povinnost definovat korky, které otestují všechna možná chybná zadání standardních příkazů. Aby byl váš návrh dokonalý, měli byste nyní doplnit kroky scénáře, který obdobným způsobem otestuje i korektní reakci hry na chybná zadání vašich nestandardních příkazů. Někdy je ale uvedení hry do stavu, v němž je možno plně otestovat některý z nestandardních příkazů, poměrně zdlouhavé. V takové situaci může být výhodnější opustit scénáře a definovat separátní testovací třídu pro otestování reakcí na všechna chybná zadání daného příkazu. Tento postup je výhodnější v situaci, kdy vaše testovací třída může uvést hru do potřebného stavu mnohem rychleji a efektivněji, než by bylo možno prostřednictvím emulace průběhu hry. Nepříjemné však u toho je, že takto navržené testy většinou neprovedou kompletní test výsledného stavu hry. Výhodou práce se scénáři je naopak to, že uvedou hru do požadovaného stavu přirozeným způsobem a že v rámci jednoho scénáře lze postupně realizovat celou řadu testů, jak je to předvedeno např. v chybovém scénáři.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 168 z 240
Kapitola 10: Realizace rozhovoru 10.
169
Realizace rozhovoru
Kapitola 10 Realizace rozhovoru
Co se v kapitole naučíme V této kapitole se soustředíme na to, jak naprogramovat rozhovor hráče s některým z objektů hry. Ukážeme si, jak na dobu rozhovoru odstavit správce příkazů a jak jej po skončení rozhovoru opět spustit.
10.1 Specifika rozhovoru Naše hra spouštěná podle úspěšného scénáře doběhla až do okamžiku, kdy má lednička zahájit rozhovor a hráčem, aby si ověřila, že je plnoletý. Problémem rozhovoru je to, že jej není možné řídit dosavadními prostředky pro reakci na zadávané příkazy. Věty, kterými hráč oslovuje nějaký objekt či kterými odpovídá na jeho dotazy, není vhodné zpracovávat stejně jako příkazy, tj. definovat objekty zodpovědné za tuto reakci tak, že budou jednoznačně určeny prvním proneseným slovem. V takovém rozhovoru totiž často nejde dopředu přesně specifikovat, jaké to první slovo bude. Při programování chování programu v průběhu rozhovoru si musíme uvědomit, že rozhovor je třeba zpracovávat poněkud jinak, a přepnout proto zpracovávání odpovědí hráče do jiného režimu. Tady trochu záleží na tom, kdo vlastně ten rozhovor začíná.
Zahajuje-li
rozhovor nějaký objekt, je to jednodušší, protože změnu režimu můžeme zapracovat do reakce objektu, jejímž výsledkem je zahájení onoho rozhovoru.
Zahajuje-li rozhovor hráč, musíme to zařídit tak, abychom se ve výsledku dostali do stavu popsaného v předchozím bodu. Nejlepší je definovat nějaký příkaz, kterým rozhovor zahájíme a součástí jeho zpracování bude i ona potřebná změna režimu zpracování reakcí hráče – např. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 169 z 240
170
Návrh semestrálního projektu a jeho rámce – Adventura oslov kouzelný_strom
Situace v naší hře odpovídá prvnímu bodu, protože rozhovor zahajuje lednička v okamžiku, kdy si z ní chce hráč vzít nějaký alkoholický nápoj. Pojďme se tedy podívat, jak bychom mohli takovouto úlohu naprogramovat.
10.2 Přepnutí do konverzačního režimu První, co budeme muset upravit, je metoda execute(String...) ve třídě CommandPickUp. Musíme zabezpečit, aby se hra v situaci, kdy hráč chce odebrat z ledničky alkoholický nápoj a lednička jej nemá ještě prověřeného, přepnula do konverzačního režimu, v němž si ověří, že hráč je již plnoletý.
Zavedení příznaku prověřenosti Když se zamyslíme nad předchozím popisem požadované reakce, tak by nás mělo napadnout, že bychom asi měli ještě před úpravou metody execute(String...) definovat příznak, v němž si bude lednička pamatovat, zda již ověřila, že hráč je plnoletý, aby se jej při každém pokusu o odebrání alkoholického nápoje neptala znovu. Jak si jistě pamatujeme, pro chovávání hodnot nejrůznějších příznaků jsme definovali knihovní třídu Flags. Definujeme v ní proto příznak isMajor spolu s příslušnými přístupovými metodami. Současně nezapomeneme přidat inicializaci tohoto příznaku do metody initialize(), aby na počátku příštího spuštění hry byl hráč opět neprověřený.
Úprava metody execute(String...) ve třídě CommandPickUp Příznak prověřenosti máme tedy definován (a inicializován), takže se můžeme soustředit na vlastní přepnutí do konverzačního režimu, které musíme začlenit do reakce na příkaz zvednutí objektu, jejíž dosavadní podobu si můžete prohlédnout ve výpisu 8.2 na straně 144. Na metodě toho moc měnit nemusíme. Počáteční prověřování, jestli je odebíraný objekt v daném prostoru, je zvednutelný a hráč má dostatek místa v batohu, můžeme ponechat. Pouze upravíme finální fázi těsně před vypsáním zprávy uživateli. Když už je ověřeno splnění výše popsaných parametrů, tak ještě zjistíme, jestli se náhodou nejedná o odebírání alkoholického nápoje z ledničky neprověřeným uživatelem. Pokud ano, zapřede s ním program konverzaci, při níž se bude snažit zjistit, je-li uživatel plnoletý. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 170 z 240
171
Kapitola 10: Realizace rozhovoru
Upravenou verzi definice této metody naleznete ve výpisu 10.1. Pojďme si ji ještě zrychleně projít:
Až do řádku 27 se původní kód nemění. V tuto chvíli je vše potřebné ověřeno, takže se můžeme zeptat, jestli se náhodou nejedná o výše popsanou speciální situaci (test na řádcích 28 a 29).
Pokud ano (předchozí test i reakce v případě, kdy je splněn, jsou ve výpisu zvýrazněny):
Vyjmeme zpět z batohu objekt (řádek 31), který jsme do něj zkušebně vložili při ověřování, zda se do něj vejde (řádek 26).
Spustíme konverzaci, přičemž musíme metodě, která konverzaci řídí, předat objekt, o nějž hráč ledničku požádal, aby mu jej mohla po úspěšně skončené konverzaci předat.
Pokud ne, dokončíme metodu standardním způsobem Výpis 10.1: Upravená definice metody execute(String...) ve třídě CommandPickUp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
/*************************************************************************** * Přesune objekt zadaný v parametru z aktuálního prostoru do batohu. * Vyžaduje však, aby objekt byl zvednutelný a do batohu se vešel. * Pokud je však odebíraným objektem alkoholický nápoj v ledničce * a plnoletost hráče ještě není ověřena, zapřede s ním lednička rozhovor. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zOBJEKT_NEZADAN; } String objectName = arguments[1]; Room currentRoom = Apartment.getInstance().getCurrentArea(); Hands bag = Hands.getInstance(); Thing object = currentRoom.getObject(objectName); if (object == null) { return zNENÍ_OBJEKT + objectName; } if (object.getWeight() >= bag.getCapacity()) { return zTĚŽKÝ_OBJEKT + objectName; } boolean added = bag.tryAddObject(object); if (added) { if (currentRoom.equals(Apartment.getInstance().getRoom(LEDNIČKA)) && object.isAlcoholic() && ! Flags.isMajor()) {
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 171 z 240
172
Návrh semestrálního projektu a jeho rámce – Adventura
31 32 33 34 35 36 37 38 39 40 }
bag.removeObject(object); return Conversation.start(object);
} currentRoom.removeObject(object); return zZVEDNUTO + objectName;
} else { return zBATOH_PLNÝ; }
10.3 Třída Conversation Pro realizaci konverzace definujeme zvláštní třídu, kterou nazveme Conversation. Můžeme se rozhodnout, zda vším pověříme objekt třídy, anebo od ní necháme vytvořit instanci, která pak bude mít vše na starosti. Při rozhodování o tom, zda dát přednost objektu třídy či její instanci je nejdůležitější to, jestli někdy budeme potřebovat daný objekt ukládat do proměnné. Pokud ano, není z čeho vybírat, protože objekt třídy v Javě do proměnné neuložíme. (Pravda, můžeme do ní uložit její class-objekt, ale volání metod prostřednictvím class-objektu je poměrně neefektivní.) Protože prozatím nevidím žádný důvod pro to, abych objekt, zodpovědný za řízení konverzace, někam ukládal, rozhodl jsem se zvolit jednodušší řešení – žádnou instanci nevytvářet a pověřit řízením konverzace objekt třídy. Kdybych později zjistil, že to nebylo dobré rozhodnutí, mohu to změnit.
Metoda start(Thing) Metoda, která odstartuje konverzaci, musí udělat následující:
Musí především nastavit příznak specifikující, že hra probíhá v režimu konverzace.
Musí si zapamatovat nápoj, který chtěl hráč z ledničky odebrat, aby mu jej objekt řídící konverzaci mohl po skončené konverzaci vydat.
Musí
připravit zprávu oznamující hráči, proč mu nápoj nebyl vydán, a co musí udělat pro to, aby jej získal. Tato zpráva musí respektovat zadání ve scénáři.
Jak vidíte, není to naštěstí nic složitého. Zdrojový možné podoby této metody si můžete prohlédnout ve výpisu 10.2. (Na konci výpisu je za příkazem return vložen komentář, v němž je naznačena odpověď hry v textové podobě.) 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 172 z 240
173
Kapitola 10: Realizace rozhovoru Výpis 10.2: Definice metody start(Thing) ve třídě Conversation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*************************************************************************** * Odstartuje rozhovor o nemožnosti jednoduchého odebrání zadaného objektu, * který je alkoholickým nápojem. * * @param drink Objekt, který chce hráč odebrat z ledničky * @return První část rozhovoru pronesená ledničkou */ static String start(Thing drink) { Flags.setConversation(true); Conversation.drink = drink; String drinkName = drink.getName(); // // // }
return zBERE_ALKOHOL + drinkName + "." + zKOLIK_LET; "Pokoušíte si vzít z inteligentní ledničky " + drinkName + "Tato lednička neumožňuje podávání alkoholických nápojů " + "mladistvým.\nKolik je vám let?");
10.4 Úprava metody executeCommand(String) ve třídě ACommand Definicí objektu zodpovědného za řízení konverzace však ještě naši úlohu neřeší. Musíme myslet na to, že veškerá komunikace s uživatelem probíhá přes metodu executeCommand(String) správce příkazů (její dosavadní podobu najdete ve výpisu 8.4 na straně 147). Ta rozhoduje o tom, kdo bude pověřen vlastním zpracováním reakce na uživatelovo zadání. Musíme proto upravit ještě tuto metodu, aby do svého rozhodování zakomponovala i informaci o tom, neprobíhá-li právě nějaká konverzace. Metoda se nejprve rozhoduje podle toho, jestli hra znova běží. Toto počáteční rozhodování bychom ponechali. Pokud hra běží, zjišťuje, nezadal-li uživatel prázdný příkaz. Tady je již na naší úvaze, jestli na prázdnou odpověď v průběhu rozhovoru zareagujeme stejně, jako bychom na ni zareagovali v režimu zadávání příkazů. Já bych se přimlouval za to, aby objekt zodpovědný za vedení rozhovoru, řešil všechny situace včetně oné prázdné odpovědi. Rozhodneme-li se pro tuto variantu, mohla by upravená verze dispečerské metody executeCommand(String) vypadat podle výpisu 10.3 (nový kód je ve výpisu zvýrazněn).
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 173 z 240
174
Návrh semestrálního projektu a jeho rámce – Adventura
Výpis 10.3: Upravená definice metody executeCommand ve třídě ACommand 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
/*************************************************************************** * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. * * @param command Zadávaný příkaz * @return Textová odpověď hry na zadaný příkaz */ static String executeCommand(String command) { command = command.trim(); String answer; if (isAlive) { if (Flags.isConversation()) { answer = Conversation.answer(command); } else if (command.isEmpty()) { answer = zPRÁZDNÝ_PŘÍKAZ; } else { answer = executeCommonComand(command); } } else { answer = startGame(command); } return answer; }
10.5 Definice metody answer(String) ve třídě Conversation Předchozí definice je však zatím nepřeložitelná, protože jsme ve třídě Conversation ještě nedefinovali metodu answer(String), kterou tato definice volá. Pojďme to tedy napravit.
Stavy rozhovoru První věcí, nad níž se musíme zamyslet, je zapracování toho, že konverzace většinou probíhá různými stavy. Jeden účastník něco řekne, druhý na to nějak zareaguje a první účastník reaguje na tuto reakci. U složitějších průběhů je vhodné si nakreslit stavový diagram a pomocí něj si ujasnit, jak přesně budou přechody mezi jednotlivými stavy probíhat.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 174 z 240
175
Kapitola 10: Realizace rozhovoru
Možné stavy našeho rozhovoru jsou tak jednoduché, že bychom pro ně žádný diagram kreslit nemuseli. Z cvičných důvodů si jej ale nakreslíme. Připomenu jenom možný průběh rozhovoru: 0. Při odstartování rozhovoru se program hráče zeptá na jeho věk a přesune se
do stavu, v němž očekává zadání věku. 1. Lze-li odpověď hráče považovat za zadání aktuálního věku, mohou nastat
dvě různá pokračování:
Nelze-li odpověď hráče považovat za zadání věku, program požádá hráče o opravu. Zůstává nadále v tomto stavu a čeká na zadání věku hráče.
Je-li zadán nízký věk, lednička odmítne hráči požadovaný alkoholický nápoj vydat.
Je-li zadán akceptovatelný věk, program se zeptá hráče na rok jeho narození a přesune se do stavu, v němž očekává zadání tohoto roku. 2. Ve stavu očekávajícím zadání roku narození je opět několik možností pokra-
čování:
Nelze-li odpověď hráče považovat za zadání roku narození, program požádá hráče o opravu. Zůstává nadále v tomto stavu a čeká na zadání roku narození hráče.
Odpovídá-li zadaný rok narození dříve zadanému věku, vydá lednička hráči požadovaný nápoj a zapamatuje si, že hráč je prověřený a bude mu proto ochotna vydávat alkoholické nápoje i příště.
Neodpovídá-li zadaný rok narození dříve zadanému věku, lednička odmítne hráči požadovaný alkoholický nápoj vydat. To, že hráč v prvním či druhém kole neprošel prověrkou, si lednička nepamatuje a umožní mu se příště znovu pokusit prověrkou projít. Stavový diagram zachycující výše popsaný přechod mezi stavy si můžete prohlédnout na obrázku 10.1. Špatně
ŘekniVěk
Špatně
Číslo
ŘekniRok
Prověřený Odpovídá Neodpovídá
Malé číslo
Obrázek 10.1 Stavový diagram rozhovoru – přechody mezi stavy v závislosti na odpovědi hráče
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 175 z 240
176
Návrh semestrálního projektu a jeho rámce – Adventura
Reakce v závislosti na stavu Nyní je třeba ještě vyřešit reakci programu v závislosti na stavu, do nějž se rozhovor dostal. Možností řešení je (jako obyčejně) celá řada:
Strukturovaně
orientovaný programátor by si zapamatovat aktuální stav v nějaké proměnné a pak se podle hodnoty této proměnné rozhodoval, jaké pokračování zvolíme. K tomuto rozhodování by použil nejspíš přepínač.
Objektově
orientovaný programátor by pro každý stav definoval objekt (např. jako hodnotu výčtového typu či instanci anonymní třídy). Každý z těchto objektů by měl vlastní verzi metody definující požadovanou reakci. V nějaké proměnné bychom pak uchovávali objekt odpovídající aktuálnímu stavu a pro reakci na odpověď hráče bychom zavolali jeho metodu.
Funkcionálně
orientovaný programátor by nepotřeboval balit spouštěnou metodu do objektu, ale ukládal by do oné proměnné přímo odkaz na funkci definující požadovanou reakci v daném stavu rozhovoru.
Protože už máme v jazyku nástroje, umožňující pracovat jednoduše s metodami jako s objekty, přikloníme se k poslední variantě. Pro onu stavovou proměnnou definujeme atribut (konverzaci řeší objekt třídy, takže atribut musí být statický) private static Function<String, String> stateDependentAnswer; do nějž uložíme odkaz na funkci, které předáme v parametru text s odpovědí hráče a která vrátí text s odpovědí hry. Každá metoda pak nastaví atribut na metodu odpovídající stavu, do kterého hra přešla. V naší hře máme pouze dva přechodné stavy, takže definujeme dvě metody. Pro zpracování reakce na opověď hráče ve stavu, kdy po něm hra chtěla, aby ji prozradil svůj věk, bychom definovali metodu private static String waitingForTheAge(String userAnswer) a pro zpracování reakce ve stavu, kdy po hráči chceme, aby nám zadal rok svého narození, metodu private static String waitingForTheYear(String userAnswer) Prozatím definujeme obě metody s poloprázdným tělem vyhazujícím výjimku unsupportedOperationException. Současně musíme upravit metodu start(Thing) (viz výpis 10.2 na straně 173) tak, abychom před vrácením odpovědi nastavili hodnotu stavové proměnné příkazem stateDependentAnswer = Conversation::waitingForTheAge;
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 176 z 240
177
Kapitola 10: Realizace rozhovoru
Vlastní definice metody answer(String) Všechny předchozí úvahy vedou k superjednoduché definici obsahující jediný příkaz, který zavolá metodu uloženou ve stavové proměnné stateDependentAnswer – viz výpis . Výpis 10.4: Definice metody answer(String) ve třídě Conversation 1 2 3 4 5 6 7 8 9 10
/*************************************************************************** * Metoda řešící reakce hry na odpovědi hráče v průběhu konverzace. * * @param command Odpověď hráče na předchozí otázku * @return Odpověď hry hráči */ static String answer(String command) { return stateDependentAnswer.apply(command); }
Test Takže bychom měli mít vše hotovo pro to, abychom mohli spustit test, jestli jsme se ve scénáři dostali o krok dál. Po spuštění testu se dozvíme, že 19. krok se nám opravdu podařilo projít a test byl ukončen vyhozením výjimky: Ukončení bylo způsobeno vyhozením výjimky: cz.pecinovsky.adv_framework.test_util.comon.TestException: Při vykonávání příkazu: «20» vyhodila hra výjimku at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyScenarioStep(GameTRunTest.java:30 0) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.executeScenario(GameTRunTest.java:200) ... Caused by: java.lang.UnsupportedOperationException: Not supported yet. at org.edupgm.adventure.Conversation. waitingForTheAge(Conversation.java:84) Zdá se, že se nám prozatím podařilo vše naprogramovat správně (nebo alespoň tak, aby prošly testy) Ze zprávy vyplývá, že test podle očekávání zhavaroval na nedokončené definici metody waitingForTheAge. Pojďme tedy na ni.
10.6 Definice metody waitingForTheAge(String) Metoda waitingForTheAge(String) bude zodpovědná za reakci hráče v první fázi konverzace, tj. ve stavu, kde po hráči chceme, aby nám prozradil svůj věk. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 177 z 240
178
Návrh semestrálního projektu a jeho rámce – Adventura
Metoda musí nejprve ověřit, že hráč zadal svůj věk opravdu jako celé číslo za zadaného rozsahu. Není-li tomu tak, upozorní jej na to. Konverzace přitom zůstává ve stejném stavu, protože se nadále čeká na zadání věku hráče. Zadá-li hráč místo čísla nějaký text, metoda převádějící řetězec na číslo vyhodí výjimku NumberFormatException. V reakci na ni musí metoda upozornit hráče na to, co se od něj očekává. Stejné upozornění bude metoda vracet i tehdy, když hráč zadá nějaký nesmyslný věk – např. -3 roky. Je proto rozumné, aby tento test vyhodil stejnou výjimku, aby se oba druhy špatných zadání ošetřovaly na jednom místě. Zadá-li hráč smysluplný věk, bude pokračování záviset na zadané hodnotě. Nebude-li hráč plnoletý, metoda mu zdvořile oznámí, že mu alkoholický nápoj vydat nemůže, požádá jej, aby si vybral něco jiného, a ukončí konverzaci. Bude-li hráč plnoletý, metoda nastaví další stav konverzace a položí hráči kontrolní otázku na rok jeho narození. Odpověď na ni pak bude řešit metoda zodpovědná za reakci ve druhém stavu konverzace. Aby tato metoda mohla porovnat zadaný rok narození s dříve zadaným věkem, musíme si zadaný věk zapamatovat. Proto musíme zadanou hodnotu věku uložit do pomocného atributu, který definujeme příkazem private static int age; Zdrojový kód metody navržený na základě předchozích úvah si můžete prohlédnout ve výpisu 10.5. Výpis 10.5: Definice metody waitingForTheAge(String) ve třídě Conversation 1 /*************************************************************************** 2 * Metoda řešící reakci hry na odpovědi uživatele ve fázi, 3 * kdy má uživatel zadat svůj věk. 4 * 5 * @param command Odpověď uživatele 6 * @return Odpověď hry hráči 7 */ 8 private static String waitingForTheAge(String userAnswer) 9 { 10 try { 11 age = Integer.parseInt(userAnswer); 12 if ((age < LOW_AGE) || (HIGH_AGE < age)) { 13 throw new NumberFormatException(); 14 } 15 }catch(NumberFormatException nfe) { 16 return String.format(fWRONG_INTEGER + fNOT_ALLOWED + fONCE_MORE, 17 "věk", LOW_AGE, HIGH_AGE); 18 } 19 if (age < 18) { 20 Flags.setConversation(false); 21 return String.format(fNOT_ALLOWED, drink.getName()); 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 178 z 240
179
Kapitola 10: Realizace rozhovoru 22 23 24 25 }
} stateDependentAnswer = Conversation::waitingForTheYear; return zNAROZEN;
V předchozí definici najdete ještě jednu novinku. Výstupní text se zde nezadává jako textová konstanta či složenina několika textových konstant, ale začleněním zadaných dat prostřednictvím předpřipraveného formátovacího řetězce. Do třídy Texts byly jako konstanty zadány části tohoto formátovacího řetězce: /** Formáty zpráv vypisovaných v reakci na některé příkazy. */ static final String fWRONG_INTEGER = "\nMusíte zadat svůj %s jako celé číslo", fWRONG_RANGE = " v rozsahu od %d do %d", fONCE_MORE = ".\nZkuste to ještě jednou.", fNOT_ALLOWED = "\nVhledem k vašemu věku vám bohužel nemohu %s vydat." + "\nVemte si něco jiného nebo zavřete ledničku."; První formátovací řetězec je přitom složen ze tří částí proto, aby z nich bylo možno poskládat i řetězec pro následující metodu, která již nebude prozrazovat rozsah akceptovatelných čísel.
Test Po dokončení definice spustíme pravidelný test. Ten nám prozradí, že jsme se opravdu dostali o krok dál, protože byl ukončen vyhozením výjimky: Ukončení bylo způsobeno vyhozením výjimky: cz.pecinovsky.adv_framework.test_util.comon.TestException: Při vykonávání příkazu: «1994» vyhodila hra výjimku at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.verifyScenarioStep(GameTRunTest.java:30 0) at cz.pecinovsky.adv_framework.test_util.gamet.GameTRunTest.executeScenario(GameTRunTest.java:200) Caused by: java.lang.UnsupportedOperationException: Not supported yet. at org.edupgm.adventure.Conversation.waitingForTheYear(Conversation.java:122) Jinými slovy: test zhavaroval na waitingForTheYear(String). Pojďme tedy na ni.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
nehotové
definici
metody
Strana 179 z 240
180
Návrh semestrálního projektu a jeho rámce – Adventura
10.7 Definice metody waitingForTheYear(String) Specifikum definice metody waitingForTheYear(String) tkví v tom, že metoda musí zjistit, jestli zadaný rok narození odpovídá dříve zadanému věku. Metoda proto musí nejprve zjistit aktuální rok, od něj odečíst zadaný věk a tak spočítat odhadovaný rok narození. Ten pak porovná s rokem zadaným hráčem, a nebudou-li se lišit o více než o jedničku, prohlásí, že bude hráči věřit, že je plnoletý. Zdrojový kód metody navržený na základě předchozích úvah si můžete prohlédnout ve výpisu 10.6. Výpis 10.6: Definice metody waitingForTheYear(String) ve třídě Conversation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
/*************************************************************************** * Metoda řešící reakci hry na odpovědi uživatele ve fázi, * kdy má uživatel zadat rok svého narození. * * @param command Odpověď uživatele * @return Odpověď hry hráči */ private static String waitingForTheYear(String userAnswer) { int year; try { year = Integer.parseInt(userAnswer); }catch(NumberFormatException nfe) { return String.format(fWRONG_INTEGER + fONCE_MORE, "rok narození"); } Flags.setConversation(false);
}
int thisYear = LocalDate.now().getYear(); int countedAge = thisYear - year; if (Math.abs(age - countedAge) > 1) { return String.format(fDOES_NOT_MATCH + fNOT_ALLOWED, age, year, drink.getName()); } Flags.setMajor(true); Apartment.getInstance().getCurrentArea().removeObject(drink); Hands .getInstance().tryAddObject(drink); return zODEBRAL + drink.getName() + zNEZAPOMEŇ;
Test Opět spustíme verifikační test. Ten nám oznámí, že:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 180 z 240
181
Kapitola 10: Realizace rozhovoru ============================================================================= Při testu následujícího, tj. 22. kroku: Zavři Lednička se objevila CHYBA: verifyMessage ============================================================================= Začátky očekávané a obdržené zprávy nesouhlasí. Čekám: «Úspěšně jste zavřel(a) ledničku.» Přišlo: « Tento příkaz neznám.
Jak vidíme, další krok prošel a test se tentokrát zarazil na tom, že jsme ještě nedefinovali příkaz pro zavření ledničky. Pojďme to napravit.
10.8 Příkaz pro zavření ledničky – CommandCLose S příkazem pro zavření ledničky je to obdobné jako s příkazem pro její otevření. Opět je třeba definovat novou třídu a nechat správce příkazů vytvořit její instanci. Opět bude potřeba prověřit splnění některých podmínek (je zadáno, co se má zavřít, je to lednička a je v danou chvíli aktuálním prostorem) a opět bude potřeba definovat několik nových textových konstant zastupujících texty neuvedené ve správci scénářů, texty, jimiž program reaguje na špatně zadané příkazy. Předpokládám, že v současné době jste již ve stavu, kdy byste danou třídu dokázali definovat sami. Pro jistotu ale ve výpisu 10.7 uvádím zdrojový kód její metody execute(String...). Výpis 10.7: Definice metody execute(String...) instancí třídy CommandCLose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/*************************************************************************** * Zavře zadaný objekt, kterým musí být lednička a přesune hráče do kuchyně. * Vyžaduje však, aby lednička byla v daném okamžiku aktuálním prostorem. * * @param arguments Parametry příkazu * @return Text zprávy vypsané po provedeni příkazu */ @Override public String execute(String... arguments) { if (arguments.length < 2) { return zZAVÍRANÝ_OBJEKT_NEZADÁN; } Apartment apartment = Apartment.getInstance(); String roomName = arguments[1]; Room currentRoom = apartment.getCurrentArea(); if (! currentRoom.getName().equalsIgnoreCase(roomName)) { return zNENÍ_AKTUÁLNÍM_PROSTOREM; } if (! roomName.equalsIgnoreCase(LEDNIČKA)) { return zZAVŘÍT_LZE_JEN_LEDNIČKU;
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 181 z 240
182
Návrh semestrálního projektu a jeho rámce – Adventura
22 23 24 25 26 }
} Room newCurrent = apartment.getRoom(KUCHYŇ); Apartment.getInstance().setCurrentArea(newCurrent); return zZAVŘEL_LEDNIČKU;
Test Po spuštění pravidelného testu se tentokrát dozvíme, že oba scénáře prošly a že jsme tedy základní verzi plánované aplikace úspěšně dokončili.
10.9 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 9.10 Co jsme prozatím naprogramovali na straně 166, jsme přidali následující:
Upravili
jsme definici metody execute(String...) ve třídě CommandPickUp tak, aby zabezpečila, že se hra v situaci, kdy hráč chce odebrat z ledničky alkoholický nápoj a lednička jej nemá ještě prověřeného, přepne do konverzačního režimu, v němž si ověří, že hráč je již plnoletý.
Definovali jsme třídu Conversation¸ jejíž objekt je zodpovědný za řízení konverzace s hráčem.
Upravili jsme definici metody executeCommand(String) ve třídě ACommand tak, aby v režimu konverzace automaticky předávala řízení objektu zodpovědnému za řízení této konverzace.
Ujasnili jsme si stavový diagram konverzace a požadavky na metody zabezpečující správnou reakci hry v jednotlivých etapách konverzace.
Aplikovali jsme funkcionální přístup k řízení přechodu mezi stavy a zavedli atribut obsahující odkaz na metodu, která má zabezpečující správnou reakci hry v aktuálním stavu s tím, že tato metoda připraví případný přechod do dalšího stavu uložením nového odkazu do tohoto atributu.
Definovali jsme třídu CommandCLose, jejíž instance je zodpovědná za správnou reakci na příkaz pro zavření ledničky. Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 10.2.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 182 z 240
Kapitola 10: Realizace rozhovoru
183
Obrázek 10.2 Aktuální diagram tříd projektu poté, co byly definována třídy realizující podporu rozhovoru
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A10z_Conversation.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 183 z 240
184 11.
Návrh semestrálního projektu a jeho rámce – Adventura Definice uživatelského rozhraní
Kapitola 11 Definice uživatelského rozhraní
Co se v kapitole naučíme V této kapitole přidáme k naší hře uživatelské rozhraní, aby si ji mohl zahrát i běžný uživatel a ne jenom náš testovací program. Vyzkoušíme přitom několik verzí rozhraní.
11.1 Co je třeba navrhnout Základní program hry jsme rozchodili. Prozatím jej však jsou schopny spouštět pouze testy. K tomu, aby si naši hru mohl zahrát i řadový uživatel, potřebujeme definovat ještě nějaké uživatelské rozhraní, prostřednictvím nějž bude uživatel s hrou komunikovat. V současné době používá většina aplikací různá důmyslná grafická uživatelská rozhraní, která se snaží komunikaci s programem co nejvíce usnadnit. K jejich návrhu jsou však potřeba jisté znalosti a zkušenosti, a proto se touto cestou prozatím nevydáme. Naším cílem bude vytvoření co nejjednoduššího rozhraní, které můžeme následně vylepšovat tak, abychom dosáhli s minimální námahou maximální funkčnost. Při návrhu uživatelského rozhraní půjdeme dokonce ještě dál: navrhneme je tak, aby toto rozhraní bylo schopno zprostředkovat komunikaci s libovolnou hrou vyhovujícím požadavkům rámce, tj. hrou, jejíž mateřská třída implementuje interfejs cz.pecinovsky.adv_framework.game_txt.IGame.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 184 z 240
185
Kapitola 11: Definice uživatelského rozhraní
11.2 Úprava rozdělení do balíčků Doposud jsme měli všechny třídy umístěné v jednom balíčku. Tato koncepce vyhovovala do okamžiku, dokud definované třídy a jejich instance vzájemně spolupracovaly. Třídy uživatelského rozhraní, které se chystáme definovat nyní, ale už komunikaci se všemi třídami hry tak bezpodmínečně nevyžadují. Má-li být uživatelské rozhraní opravdu tak univerzální, jak jsem před chvílí naznačoval, mělo by mu stačit, bude-li mít přístup ke třídě hry. V našem balíčku proto definujeme podbalíček game, do nějž přemístíme všechny doposud definované třídy s výjimkou třídy GSMFactory, jejíž instance nám v případě potřeby dokáže dodat instanci hry i instanci správce scénářů. Vedle něj vytvoříme balíček textui, do nějž budeme ukládat třídy definující uživatelské rozhraní. Abychom mohli funkci těchto tříd snadno otestovat, přidáme do nich i metodu main(String[]). Ve společném rodičovském balíčku pak definujeme třídu Main, což bude hlavní třída našeho projektu, jež bude řešit otázku, které z našich uživatelských rozhraní se pro danou seanci použije.
11.3 Uživatelské rozhraní využívající služeb třídy javax.swing.JOptionPane Začneme třídou využívající služeb třídy javax.swing.JOptionPane, která nabízí několik metod umožňujících velmi jednoduše definovat uživatelské rozhraní využívající jednoduchá dialogová okna. Definujme proto novou třídu a nazvěme ji UIA_JOptionPane. Písmeno A je v názvu proto, že je to první ze tříd, které zde budeme definovat. Názvy dalších třídy pak budou mít za úvodním UI další písmeny abecedy. Abychom vyhověli požadavkům rámce, měla by třída definující uživatelské rozhraní implementovat interfejs cz.pecinovsky.adv_framework.game_txt.IUI definující požadavky na takovouto třídu. Tento interfejs vyžaduje implementaci dvou metod:
void
startGame() Spouští implicitní hru, což by měla být hra definovaná tvůrcem daného rozhraní.
void
startGame(IGame game) Spouští hru zadanou v parametru.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 185 z 240
186
Návrh semestrálního projektu a jeho rámce – Adventura
Prostřednictvím implementace druhé z metod dané uživatelské rozhraní dokazuje, že je schopno pracovat s libovolnou hrou vyhovující rámci, tj. implementující interfejs IGame. První metoda pak umožňuje, aby uživatel, jehož hra má některé dodatečné vlastnosti přesahující požadavky rámce, mohl definovat své uživatelské rozhraní tak, aby tyto požadavky dokázalo využít. Navíc zjednodušuje spouštění této hry.
Definice metody startGame() Protože naše hra nemá žádné speciální vlastnosti, pro jejichž plné využití by bylo potřeba mít nějaké zvláštně upravené uživatelské rozhraní, můžeme bezparametrickou verzi spouštěcí metody definovat jednoduše tak, že zavoláme jednoparametrickou verzi, které v parametru předáme instanci naší hry. Takováto definice by mohla vypadat např. podle výpisu 11.1. Výpis 11.1: Definice metody startGame() ve třídě UIA_JOptionPane 1 2 3 4 5 6 7 8 9
/*************************************************************************** * Spustí komunikaci mezi implicitní hrou * a danou instancí uživatelského rozhraní. */ @Override public void startGame() { startGame(OfficialApartmentGame.getInstance()); }
Metody třídy JOptionPane Definice jednoparametrické metody bude o maličko složitější, ale ne o moc. Pro komunikaci s uživatelem můžeme použít statickou metodu showInputDialog(Component parentComponent, Object message) Jejím prvním parametrem je komponenta (např. okno), nad jejímž středem otevře dialogové okno (tato komponenta je označována jako rodičovská). Druhým parametrem metody je text, který uživateli většinou vysvětluje, co má zadat. Metoda otevře dialogové okno se vstupním textovým polem (viz obrázek ), do kterého uživatele zadá text, který pak metoda vrátí.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 186 z 240
Kapitola 11: Definice uživatelského rozhraní
187
Obrázek 11.1 Dialogové okno s úvodní zprávou hry čekající na zadání příkazu
Je-li prvním parametrem metody hodnota null, okno se otevře ve středu primární obrazovky. Většině uživatelů to stačí, ale pokud potřebujete otevřít dialogové okno na nějakém specifickém místě, zejména pak na jiném než primárním monitoru, musíte první parametr zadat. Nejjednodušší je vyrobit prázdné okno, umístit je do požadované pozice a zobrazit (nebude-li zobrazené, naše dialogové okno je bude ignorovat). Předáme-li toto okno v prvním parametru metody showInputDialog, zobrazí se požadované okno nad tímto pomocným oknem a zakryje je, takže pomocné okno nebude rušit. (Tedy alespoň do doby, dokud ono dialogové okno někam neodsunete.) Pro vytvoření takovéhoto pomocného okna můžeme použít kód: Component parent = new JFrame(); parent.setLocation(100, 100); parent.setVisible(true); Při používání metod této třídy ale nesmíme zapomenout na to, že jakmile otevřeme nějaké okno, spustí se samostatná obsluha grafického uživatelského rozhraní, která se neukončí automaticky po ukončení metody main(String). Program proto musíme explicitně ukončit, např. zavoláním metody System.exit(int), které předáváme v parametru chybový kód. Když program končí bez chyby, předáme nulu.
Definice metody startGame(IGame) Pojďme se nyní podívat, jak naprogramovat vlastní komunikaci. Jak jistě odhadnete, budeme uživateli stále kolem dokola zobrazovat dialogové okno se zprávou hry, a budeme po něm chtít, aby v reakci na tuto zprávu zadal další příkaz. Tento příkaz předáme hře (zavoláme její metodu execute(String)), a opět otevřeme dialogové okno s její odpovědí. Tak to budeme dělat, dokud hra neskončí, což zjistíme zavoláním její metody isAlive(). Jednou z možností, jak naprogramovat takovouto opakovanou činnost, je definovat cyklus:
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 187 z 240
188
Návrh semestrálního projektu a jeho rámce – Adventura
do {
command = JOptionPane.showInputDialog(parent, answer); answer = game.executeCommand(command); } while (game.isAlive()); Otázkou zůstává, jak zjistit tu první odpověď hry. Jak si ale možná vybavíte, hra se startuje prázdným příkazem a v odpovědi na tento příkaz vrátí úvodní vítací sekvenci. Druhým problémem je, že takto ukončená hra se s hráčem vůbec nerozloučí a nepogratuluje mu k případnému vítězství. Poslední odpověď hry totiž zůstane nezpracovaná. Tak tomu ale nemusí být. Stačí přidat za cyklus volání statické metody JOptionPane.showMessageDialog(Component parentComponent, Object message) která pouze zobrazí zadanou zprávu a nebude již po uživateli chtít žádnou reakci kromě stisknutí potvrzovacího tlačítka OK. Zdrojový kód metody startGame(IGame) respektující všechny výše uvedené úvahy si můžete prohlédnout ve výpisu 11.2. Výpis 11.2: Definice metody startGame(IGame) ve třídě UIA_JOptionPane 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*************************************************************************** * Spustí komunikaci mezi zadanou hrou a danou instancí * mající na starosti komunikaci s uživatelem. * * @param game Hra, kterou ma dané UI spustit */ @Override public void startGame(IGame game) { Component parent = new JFrame(); parent.setLocation(100, 100); parent.setVisible(true);
}
String command; String answer = game.executeCommand(""); do { command = JOptionPane.showInputDialog(parent, answer); answer = game.executeCommand(command); } while (game.isAlive()); JOptionPane.showMessageDialog(parent, answer); System.exit(0);
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 188 z 240
Kapitola 11: Definice uživatelského rozhraní
189
11.4 Uživatelské rozhraní komunikující přes standardní vstup a výstup Právě naprogramované uživatelské rozhraní má jednu nevýhodu: nezobrazuje historii našich akcí. Tu bychom mohli zobrazit, kdybychom s uživatelem komunikovali prostřednictvím standardního vstupu a výstupu.
Nevýhody používání standardního vstupu a výstupu Standardní vstup a výstup má však jednu nevýhodu: nelze jej používat nezávisle na platformě. Tuto nezávislost narušuje operační systém Windows, který používá v různých režimech různá kódování. Řada programů je schopna pracovat s univerzálním kódováním UTF-8, ale standardní vstup a výstup mezi ně nepatří. Budete-li program spouštět pod nějakým vývojovým prostředím, tak to je vám schopno nabídnout vlastní verzi standardního vstupu a výstupu, která vás od těchto problémů odstíní. Budete-li však pracovat v příkazovém řádku pod Windows, budete muset tento problém vyřešit.
Třída java.util.Scanner Přímé čtení ze standardního vstupu je poměrně náročné, protože je třeba jej číst znak za znakem a definovat vlastní zpracování přečtených znaků (např. zjistit, kdy uživatel ukončil řádek, aby jej bylo možno zpracovat). Aby byly vývojáři od těchto starostí do jisté míry osvobození, zavedla Java 5 třídu Scanner, jejíž instance za nás čtení ze standardního vstupu do jisté míry předzpracují. My budeme ze třídy Scanner využívat pouze jeden z jejích deseti konstruktorů a jednu z jejích 65 metod. Potřebnou instanci budeme vytvářet prostřednictvím konstruktoru Scanner(InputStream source) kterému jako jeho parametr předáme objekt standardního vstupu System.in. Pro čtení uživatelových příkazů pak budeme používat metodu String nextLine() která přečte zbytek aktuálního řádku bez znaku ukončení řádku. Protože my nebudeme řádky zpracovávat nijak jinak, přečte nám tato metoda řádek vždy celý.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 189 z 240
190
Návrh semestrálního projektu a jeho rámce – Adventura
Vytvoření třídy pro konzolový vstup Třídu nejjednodušeji vytvoříme zkopírováním třídy UIA_JOptionPane a pojmenováním vytvořené kopie UIB_Scanner. Definici metody startGame() ponecháme beze změny a soustředíme se na metodu startGame(IGame). Kdybychom převzali koncepci ze třídy UIA_JOptionPane, vypadala by definice metody nejspíš podle výpisu 11.3: Výpis 11.3: Definice metody startGame(IGame) ve třídě UIB_Scanner 1 2 3 4 5 6 7 8 9 10 11 12 13
@Override public void startGame(IGame game) { Scanner scanner = new Scanner(System.in); String command; String answer = game.executeCommand(""); System.out.println(answer); do { command = scanner.nextLine(); answer = game.executeCommand(command); System.out.println(answer); } while (game.isAlive()); }
Tady by nám ale mohlo vadit, že se na řádcích 6 a 10 vyskytuje stejný příkaz a stejně tak na řádcích 7 a 11. Kdybychom cyklus upravili podle výpisu 11.4, tak by tato duplikace řádků odpadla. Vyberte si, které řešení je vám sympatičtější. Výpis 11.4: Definice metody startGame(IGame) ve třídě UIB_Scanner 1 2 3 4 5 6 7 8 9 10 11 12 13
@Override public void startGame(IGame game) { Scanner scanner = new Scanner(System.in); String command = ""; String answer; for(;;) { answer = game.executeCommand(command); System.out.println(answer); if (! game.isAlive()) { break; } //----------> command = scanner.nextLine(); } }
jistě někoho napadlo, že obdobná duplikace příkazů pro získání Nyní odpovědi je i ve výpisu 11.2. To je pravda, ale tam se opakuje pouze jeden
příkaz, kdežto tady se opakovaly již dva. Nicméně zájemci samozřejmě mohou příslušně upravit i kód z výpisu 11.2.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 190 z 240
Kapitola 11: Definice uživatelského rozhraní
191
11.5 Zobecnění uvedených řešení Když porovnáme řešení prostřednictvím metod třídy JOptionPane s řešeními využívajícími instanci třídy Scanner, vidíme, že mají mnoho společného. Jejich podobnost přímo vyzývá k tomu, abychom pro ně definovali společnou kostru, do níž bychom pak dosazovali konkrétní řešení. Uvedená řešení se vlastně liší pouze v inicializaci, která předchází vlastní spuštění hry, a potom ve způsobu, jakým získávají od uživatele další příkaz a způsobu, jak uživateli předávají závěrečnou zprávu hry. Kdybychom proto definovali interfejs deklarující tyto metody, pak bychom mohli definovat pro každé z uvedených řešení třídu, která tento interfejs implementuje, tak bychom mohli definovat univerzální uživatelské rozhraní, do kterého bychom pouze dosadili danou třídu. Pojďme si to ukázat.
Interfejs IGamePlayer Před chvílí jsem říkal, že se jednotlivá řešení liší ve třech detailech: inicializaci, získání dalšího příkazu a zobrazení závěrečné zprávy. První z metod ale v chystaném interfejsu deklarovat nemusíme, protože inicializaci si může vzít na starost konstruktor. Budeme-li chtít vše odstartovat znovu, vytvoříme prostě další instanci. Interfejs deklarující zbylé dvě metody bychom pak mohli definovat podle výpisu 11.5. Výpis 11.5: Definice interfejsu IGamePlayer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/******************************************************************************* * Instance interfejsu {@code IGamePlayer} definují variantní části * univerzálního textového uživatelského rozhraní * pro hraní textových konverzačních her. */ public interface IGamePlayer { //== OTHER ABSTRACT METHODS ==================================================== /*************************************************************************** * Pošle uživateli zadanou zprávu a převezme od něj další příkaz. * * @param message Posílaná zpráva * @return Uživatelem zadaný příkaz */ public String askCommand (String message); /*************************************************************************** * Pošle uživateli zadanou zprávu. *
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 191 z 240
192
Návrh semestrálního projektu a jeho rámce – Adventura
22 23 24 25 }
* @param message Posílaná zpráva */ public void sendMessage(String message);
Současně bychom mohli definovat třídu UIC_GamePlayer, jejíž konstruktor by jako parametr přebral instanci interfejsu IGamePlayer a v příslušných bodech svého algoritmu by pak použil metody této instance. Definice výkonných tříd implementujících interfejs IGamePlayer budou tak jednoduché, že bychom je mohli definovat jako interní třídy. Jako interní bychom nakonec mohli definovat i interfejs IGamePlayer, jehož jediným účelem je definovat společný rodičovský typ tříd, jejichž instance specifikují druh komunikace s uživatelem.
Hlavní metoda umožňující volbu použitého rozhraní Když ale nyní můžeme zvolit způsob realizace uživatelského rozhraní použitého v metodě řídící komunikaci mezi uživatelem a hrou, tak také musíme uživateli umožnit, aby si vybral, jakému způsobu dá přednost. K tomu je nejvýhodnější použít parametry příkazového řádku, kterým se spouští celá aplikace. Definujeme proto hodnoty parametrů, které budou specifikovat zvolený způsob, a současně zvolíme způsob, který se použije v případě, když uživatel žádný preferovaný způsob nezadá. Můžeme se rozhodnout třeba následovně:
Bude-li mít první parametr příkazového řádku parametr hodnotu –con, použije se komunikace využívající standardní vstup a výstup (konzoli) a služeb třídy Scanner.
Bude-li mít první parametr příkazového řádku parametr hodnotu –jop, použije se komunikace využívající služeb třídy JOptionPain.
Protože je druhá možnost universálnější (nemusíme řešit problémy s kódováním), zvolíme ji současně jako implicitní. Nebude-li proto mít první parametr žádnou z výše uvedených hodnot, použije se služeb třídy JOptionPain. Příslušné rozhodování o tom, který způsob komunikace s uživatele zvolit, pak bude mít na starosti metoda main(String[]).
Výsledná definice třídy UIC_GamePlayer Na základě předchozích úvah bychom již měli znát vše potřebně, abychom definovali třídu UIC_GamePlayer s hlavní metodou umožňující výběr použitého uživatelského rozhraní. Možnou definici této třídy si můžete prohlédnout ve výpisu 11.6. Jak z výpisu vidíte, vzhledem k jednoduchosti tříd definujících jednotlivé způsoby komunika49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 192 z 240
Kapitola 11: Definice uživatelského rozhraní
193
ce s uživatelem, jsou tyto třídy společně s jimi implementovaným interfejsem definovány jako interní (přesněji vnořené). Výpis 11.6: Definice třídy UIC_GamePlayer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/******************************************************************************* * Instance třídy {@code UIC_GamePlayer} realizují uživatelské rozhraní, * kterému lze zadat objekt typu {@link IGamePlayer}, jehož prostřednictvím * bude program komunikovat s uživatelem. * * @author Rudolf PECINOVSKÝ * @version 0.00.0000 — 20yy-mm-dd */ public class UIC_GamePlayer implements IUI { //== CONSTANT CLASS ATTRIBUTES ================================================= //== VARIABLE CLASS ATTRIBUTES =================================================
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= //== OTHER NON-PRIVATE CLASS METHODS =========================================== //== PRIVATE AND AUXILIARY CLASS METHODS =======================================
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Objekt specifikující některé detaily konverzace. */ private final IGamePlayer player;
//== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** * Vytvoří instanci využívající pro řešení některých detailů zadaný objekt. * * @param player Objekt pro řešení některých detailů */ public UIC_GamePlayer(IGamePlayer player) { this.player = player; }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 193 z 240
194 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
Návrh semestrálního projektu a jeho rámce – Adventura
//== ABSTRACT METHODS ========================================================== //== INSTANCE GETTERS AND SETTERS ============================================== //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** * Spustí komunikaci mezi implicitní hrou * a danou instancí uživatelského rozhraní. */ @Override public void startGame() { startGame(OfficialApartmentGame.getInstance()); } /*************************************************************************** * Spustí komunikaci mezi zadanou hrou a danou instancí * mající na starosti komunikaci s uživatelem. * * @param game Hra, kterou ma dané UI spustit */ @Override public void startGame(IGame game) { String command = ""; String answer; for(;;) { answer = game.executeCommand(command); if (! game.isAlive()) { break; } //----------> command = player.askCommand(answer); } player.sendMessage(answer); }
//== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
//############################################################################## //== NESTED DATA TYPES ========================================================= /*************************************************************************** * Instance interfejsu {@code IGamePlayer} definují variantní části * univerzálního textového uživatelského rozhraní * pro hraní textových konverzačních her. */ public interface IGamePlayer
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 194 z 240
Kapitola 11: Definice uživatelského rozhraní
195
100 { 101 /*********************************************************************** 102 * Pošle uživateli zadanou zprávu a převezme od něj další příkaz. 103 * 104 * @param message Posílaná zpráva 105 * @return Uživatelem zadaný příkaz 106 */ 107 public String askCommand (String message); 108 109 110 /*********************************************************************** 111 * Pošle uživateli zadanou zprávu. 112 * 113 * @param message Posílaná zpráva 114 */ 115 public void sendMessage(String message); 116 } 117 118 119 120 //////////////////////////////////////////////////////////////////////////////// 121 //////////////////////////////////////////////////////////////////////////////// 122 123 /*************************************************************************** 124 * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci 125 * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. 126 */ 127 public static class ByJOptionPane implements IGamePlayer 128 { 129 final Component PARENT; 130 131 /** Vytvoří novou instanci. Při té příležitosti vytvoří rodičovskou 132 * komponentu definující umístění dialogových oken. */ 133 ByJOptionPane() { 134 PARENT = new JFrame(); 135 PARENT.setLocation(100, 100); 136 PARENT.setVisible(true); 137 } 138 139 /** {@inheritDoc} */ 140 @Override public String askCommand(String message) { 141 return JOptionPane.showInputDialog(PARENT, message); 142 } 143 144 /** {@inheritDoc} */ 145 @Override public void sendMessage(String message) { 146 JOptionPane.showMessageDialog(PARENT, message); 147 } 148 } 149 150 151 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 195 z 240
196 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
Návrh semestrálního projektu a jeho rámce – Adventura //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** * Instance třídy {@code ByScanner} zprostředkovávají komunikaci * s uživatelem prostřednictvím standardního (konzolového) vstupu a výstupu, * přičemž vstup je zabalen do instance třídy {@link Scanner}. */ public static class ByScanner implements IGamePlayer { Scanner scanner = new Scanner(System.in); /** {@inheritDoc} */ @Override public String askCommand(String message) { sendMessage(message); return scanner.nextLine(); }
}
/** {@inheritDoc} */ @Override public void sendMessage(String message) { System.out.println(message); }
//############################################################################## //== MAIN METHOD ===============================================================
}
/*************************************************************************** * Metoda spouštějící hru {@link OfficialApartmentGame} umožňující zadat * prostřednictvím parametrů příkazového řádku, * zda bude použito uživatelském rozhraním využívající služeb * třídy {@link JOptionPane} nebo standardního výstupu a * standardního vstupu zabaleného do instance třídy {@link Scanner}. * * @param args Parametry příkazového řádku */ public static void main(String[] args) { IGamePlayer gamePlayer; gamePlayer = ((args.length < 1) || (! args[0].equals("-con"))) ? new ByJOptionPane() : new ByScanner(); new UIC_GamePlayer(gamePlayer).startGame(); System.exit(0); }
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 196 z 240
Kapitola 11: Definice uživatelského rozhraní
197
11.6 Další zobecnění uživatelského rozhraní Předchozí řešení bychom mohli dále zobecnit a umožnit hráči, aby si po skončení hry vybral, jestli si ji nebude chtít zahrát ještě jednou. Kdybychom rozšířili definici interfejsu o metodu zprostředkovávající tento výběr. Implementovaný interfejs IUI tuto možnost nezmiňuje, takže by se implementace jeho metod neměla měnit. Přesněji: neměl by se měnit kontrakt definující jejich chování. To nám otevírá možnost, abychom vylepšenou třídu definovali jako potomka té původní a v tomto potomkovi definovali pouze přidané členy. Definujeme proto třídu UID_Multiplayer, která bude potomkem naší předchozí třídy UIC_GamePlayer a bude k ní pouze přidávat členy týkající se rozšíření možností instancí nové třídy. Především v ní definujeme interfejs IGameMultiplayer, který bude potomkem obdobného interfejsu rodičovské třídy a který ke dvěma zděděným metodám přidá deklaraci metody wantContinue() získávající hráčovu odpověď na otázku, zda si chce zahrát ještě jednou. Současně v ní definujeme interní třídy ByJOptionPane a ByScanner, které budou potomky svých stejnojmenných protějšků a budou implementovat „vylepšený“ interfejs IGameMultiplayer – jinými slovy přidají definici třetí metody. Ve třídě UID_Multiplayer pak definujeme instanční metodu multistartGame(), která odstartuje hru, po jejím ukončení se zeptá na ochotu k nové hře a v případě kladné odezvy spustí hru znovu. Předchozí úvahy vedou k definici dceřiné třídy, která od rodičovské třídy dědí nejenom metody, ale i rodiče svých interních datových typ – interfejsu a dvou tříd. Takto definovanou třídu se můžete prohlédnout ve výpisu . Výpis 11.7: Definice třídy UID_Multiplayer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/******************************************************************************* * Instance třídy {@code UID_Multiplayer} realizují uživatelské rozhraní, * které rozšiřuje možnosti interfejsu typu {@link IGamePlayer} o možnost * zadat po skončení hry, zda si hráč chce zahrát ještě jednou, * a pokud ano, tak si také vybrat typ rozhraní, kterému bude dávat přednost. * / public class UID_Multiplayer extends UIC_GamePlayer { //== CONSTANT CLASS ATTRIBUTES ================================================= //== VARIABLE CLASS ATTRIBUTES =================================================
//############################################################################## //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== //== CLASS GETTERS AND SETTERS ================================================= //== OTHER NON-PRIVATE CLASS METHODS ===========================================
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 197 z 240
198 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
Návrh semestrálního projektu a jeho rámce – Adventura //== PRIVATE AND AUXILIARY CLASS METHODS =======================================
//############################################################################## //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Objekt specifikující některé detaily konverzace. */ private final IGameMultiplayer multiplayer;
//== VARIABLE INSTANCE ATTRIBUTES ==============================================
//############################################################################## //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** * Vytvoří instanci využívající pro řešení některých detailů zadaný objekt. * * @param multiplayer Objekt definující řešení některých detailů */ public UID_Multiplayer(IGameMultiplayer multiplayer) { super(multiplayer); this.multiplayer = multiplayer; }
//== ABSTRACT METHODS ========================================================== //== INSTANCE GETTERS AND SETTERS ============================================== //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** * Komunikuje s uživatelem prostřednictvím zadaného prostředku. * Vždy spustí hru a po jejím ukončení se uživatele zeptá, * chce-li si zahrát ještě jednou, a proku ano, znovu spustí hru. */ public void multistartGame() { do { startGame(); } while(multiplayer.wantContinue()); }
//== PRIVATE AND AUXILIARY INSTANCE METHODS ====================================
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 198 z 240
Kapitola 11: Definice uživatelského rozhraní 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
199
//############################################################################## //== NESTED DATA TYPES ========================================================= /*************************************************************************** * Instance interfejsu {@code IGameMultiplayer} definují variantní části * univerzálního textového uživatelského rozhraní * pro hraní textových konverzačních her. */ public interface IGameMultiplayer extends IGamePlayer { /*********************************************************************** * Zjistí, chce-li si uživatel zahrát ještě jednou. * * @return Chce-li si uživatel znovu zahrát, vrátí {@code true}, * jinak vrátí {@code false} */ public boolean wantContinue(); }
//////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. */ public static class ByJOptionPane extends UIC_GamePlayer.ByJOptionPane implements IGameMultiplayer { /** {@inheritDoc} */ @Override public boolean wantContinue() { int answer = JOptionPane.showConfirmDialog(PARENT, "Chcete si zahrát ještě jednou?"); return (answer == 0); } }
//////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. */ public static class ByScanner extends UIC_GamePlayer.ByScanner
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 199 z 240
200
Návrh semestrálního projektu a jeho rámce – Adventura
122 implements IGameMultiplayer 123 { 124 /** {@inheritDoc} */ 125 @Override public boolean wantContinue() { 126 String answer = askCommand("Chcete si zahrát ještě jednou (A/N)?"); 127 answer = answer.trim().toUpperCase(); 128 return (answer.charAt(0) == 'A'); 129 } 130 } 131 132 133 134 //############################################################################## 135 //== MAIN METHOD =============================================================== 136 137 /*************************************************************************** 138 * Metoda spouštějící hru {@link OfficialApartmentGame} umožňující zadat 139 * prostřednictvím parametrů příkazového řádku, 140 * zda bude použito uživatelské rozhraní využívající služeb 141 * třídy {@link JOptionPane} nebo standardního výstupu a 142 * standardního vstupu zabaleného do instance třídy {@link Scanner}. 143 * 144 * @param args Parametry příkazového řádku 145 */ 146 public static void main(String[] args) 147 { 148 IGameMultiplayer gameMultiplayer; 149 gameMultiplayer = ((args.length < 1) || (! args[0].equals("-con"))) 150 ? new ByJOptionPane() 151 : new ByScanner(); 152 new UID_Multiplayer(gameMultiplayer).multistartGame(); 153 System.exit(0); 154 } 155 }
11.7 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 10.9 Co jsme prozatím naprogramovali na straně 182, jsme přidali následující:
Rozdělili jsme řešení do dvou podbalíčků: Do podbalíčku game jsme přemístili všechny datové typy týkající se přímo definované hry, tj. definice všech doposud definovaných tříd s výjimkou tovární třídy GSMFactory.
V podbalíčku textui jsme pak definovali všechny postupně se vylepšující třídy, jejichž instance realizují uživatelské rozhraní. 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 200 z 240
Kapitola 11: Definice uživatelského rozhraní
201
Definovali jsme třídu UIA_JOptionPane, jejíž instance komunikuje s uživatelem za pomoci služeb třídy javax.swing.JOptionPane, která umožňuje komunikace prostřednictvím velmi jednoduchých dialogových oken.
Definovali jsme třídu UIB_Console, jejíž instance řeší komunikaci s uživatelem
prostřednictvím standardního vstupu a výstupu, přičemž pro snadnější zpracování vstupu používá instance třídy Scanner.
Definovali jsme třídu UIC_GamePlayer, která zobecňuje předchozí dvě řešení a definuje univerzální metodu pro komunikaci s hráčem, přičemž umožňuje specifikovat detaily této komunikace prostřednictvím objektů implementujících interní interfejs IGamePlayer.
V této třídě jsme definovali interní třídy ByJOptionPane a ByScanner, jejichž instance představují ony specifikační objekty.
Definovali jsme třídu UID_Multiplayer, v níž jsme předchozí řešení dále zobecnili tak, že hráč si nyní může zahrát hru znovu, aniž by musel ukončovat a znovu spouštět program. Třídu i její interní datové typy jsme definovali jako potomky datových typů z prvního zobecňovacího kroku, přičemž tito potomci pouze doplňují přidanou funkcionalitu, aniž by upravovali funkcionalitu svých předků. Diagram tříd balíčku s třídami hry se nezměnil, takže si pouze připomeneme současný stav balíčku s doposud definovanými třídami řešícími textové uživatelské rozhraní – najdete jej na obrázku 11.2.
Obrázek 11.2 Aktuální diagram tříd balíčku s doposud definovanými třídami řešícími textové uživatelské rozhraní
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A11z_TextUserInterface .
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 201 z 240
202
Návrh semestrálního projektu a jeho rámce – Adventura
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 202 z 240
203
Kapitola 12: Vylepšování architektury – refaktorace 12.
Vylepšování architektury – refaktorace
Kapitola 12 Vylepšování architektury – refaktorace
Co se v kapitole naučíme .
12.1 Při definici metody execute(String...) instancí třídy CommandSupportIcebox (viz výpis 9.7 na straně 165) jsem vám říkal, že metoda je nechutně dlouhá. Výpis 12.1: Definice Class 156
OBR Obrázek 12.1 Vlastní popis obrázku (pozor na počáteční tabulátor – zachovat!)
12.2 Co jsme prozatím naprogramovali Udělejme si tedy souhrn toho, co jsme doposud naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 9.10 Co jsme prozatím naprogramovali na straně 166, jsme přidali následující: 49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 203 z 240
204
Návrh semestrálního projektu a jeho rámce – Adventura
Definovali
jsme třídu Flags jako schránku na nejrůznější příznaky, které je třeba si pamatovat v průběhu hry. Každý příznak je definovaný jako statický atribut s příslušnými přístupovými metodami.
Definovali
jsme třídu CommandOpen, jejíž instance je zodpovědná za správnou reakci na příkaz pro otevření ledničky.
Opravili jsme chybu v metodě execute(String...) instancí třídy CommandPutDown. Definovali jsme třídu CommandRead, jejíž instance je zodpovědná za správnou reakci na příkaz k přečtení zadaného objektu – papíru nebo časopisu.
Definovali
jsme třídu CommandSupportIcebox, jejíž instance je zodpovědná za správnou reakci na příkaz pro podložení ledničky.
Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku 9.1.
Obrázek 12.2 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující nestandardní příkazy hry bez podpory rozhovoru
jehož zdrojové kódy odpovídají výše popsanému stavu vývoje Program, aplikace, najdete v projektu A10z_Conversation.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 204 z 240
Kapitola 13: Doporučení pro obhajobu 13.
205
Doporučení pro obhajobu
Kapitola 13 Doporučení pro obhajobu
Co se v kapitole naučíme .
13.1 Výpis 13.1: Definice Class 157
Obrázek 13.1 Popis obrázku (pozor na počáteční tabulátor – zachovat!)
13.2 Shrnutí – co jsme se naučili Zopakujme si, co jsme se v této lekci dozvěděli:
.
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 205 z 240
236
Návrh semestrálního projektu a jeho rámce – Adventura
Rejstřík
Rejstřík —A—
—L—
adventura koncepce, 18 zadání, 42 alternativní scénář, 27 analogie, 16
logger, 34
—G—
—N—
GoF, 231
návrhový vrzor Přepravka, 34
—H— hlavní scénář, 27 hra akce, 33 koncepce, 18 příkaz, 33 zadání, 42
—I— integrační test, 25 interfejs Iterable<E>, 209 iterovatelný objekt, 210, 219
—J— jednotkový test, 25
—K— kolekce, 226
—M— metoda orElse, 114
—O— objekt iterovatelný, 210, 219 Optional, 114 otElse, 114 orElse, 114
—P— Přepravka, 34
—R— reflexe, 37 rozhraní Iterable<E>, 219
—S— Scenario popis, 219 ScenarioStep popis, 28
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
scénář, 26, 219 alternativní, 27 hlavní, 27 krok scénáře, 28 typ, 26 základní, 27
—T— TDD, 25 terminologie, 15 test integrační, 25 jednotkový, 25 vývoj řízený testy, 25 Test Driven Development, 25 třída ScenarioStep, 28 typ Optional, 114 orElse, 114 typ scénáře, 26
—U— úloha, 15
—V— výpis ACommand, 90
Rejstřík getAllCommands, 127 initialize, 111, 153 startGame, 111 Apartment
237 CommandSupportIc ebox execute, 165 Conversation answer, 177 start, 173 waitingForTheAge , 178 waitingForTheYea r, 180 Flags, 153 Hands tryAddObject, 132 IAuthorPrototype, 23 IGamePlayer, 191 ManagerWithConst ants, 60 ManagerWithLiteral s, 44, 47, 50 OfficialApartmentG ame execute, 85 getAllCommands, 126 getWorld, 109 isAlive, 86 OfficialApartmentM anager
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
isAlive, 80 main, 122 REQUIRED_STEP S, 120 Room
—Z— základní scénář, 27
Strana 237 z 240
Část IV: KONEC
Část IV: KONEC
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 239 z 240
E
Odkladky
Příloha E Odkladky
49R_Adventura.doc verze 0.13.5299, uloženo: ne 12.4.15 – 21:45
Strana 240 z 240