Programování pro hračičky/Andělé/Lekce 5

Jak používat klasifikační nálepkuTato stránka je součástí kurzu:
středoškolská
Příslušnost: skupinová

Opakování a probrání domácího úkolu editovat

Projděme si, co se nám podařilo vylepšit na našich místnostech a objektech v souvislosti s domácím úkolem: Jaké zajímavé funkce jsme objevili při pátrání ve zdrojových kódech a v dokumentaci? Podařilo se nám některou z nich dokonce překrýt? Jak to ve výsledku funguje?

Definice hráčského příkazu editovat

Umíme už ovlivnit chování objektů nastavením jejich vlastností, a nově též překrytím některých funkcí. Při tom se ovšem stále spoléháme na již hotové hráčské příkazy. Můžeme například zavoláním funkce set_wield_msg() nastavit, jakou hlášku má zbraň vydávat, když ji tasíme, můžeme překrýt funkci wield() a v překrytí naprogramovat, že se nešikovný hráč při tasení s určitou pravděpodobností o zbraň sám zraní, ovšem stále při tom využíváme již hotového příkazu tas.

Pro vytváření opravdu nových věcí ve hře potřebujeme umět vytvářet příkazy vlastní. Chceme-li, aby bylo možno troubit na trubku, mačkat tlačítko, drbat se tužkou za uchem atd., musíme se naučit definovat příkazy zatrub, zmáčkni, podrbej se atd., a prověřovat, zda jimi hráč skutečně míní to, co máme na mysli my (zatrub jako jelen znamená přece něco jiného než zatrub na trubku). A právě o to se nyní pokusíme.

Jako příklad si vezmeme definici příkazu cinkej v programu standardní klíčenky (/obj/klicenka). Její jednotlivé části si pak postupně probereme a vysvětlíme:

void init()
{
  add_actions("cinkani",({"cinkej","cinkám","cinkam"}));
}

int cinkani(string s)
{
  int klicu;

  if (!s) 
    return notify_fail("Čím chceš cinkat?\n");
  if (!me(s,7)) 
    return notify_fail("Tím se nedá cinkat.\n");
  if (!(klicu=sizeof(all_inventory()))) 
    return notify_fail("Bez klíčů? Jak to chceš provést?\n");
  if (klicu==1) 
    {
      send_message_to(this_player(),MT_NOTIFY|MT_NOISE,MA_USE,
        wrap("Cinkáš "+svuj(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));
      this_player()->send_message(MT_NOISE,MA_USE,
        wrap(Ten(1,this_player())+" cink"+plural("á ","ají ",this_player())
        +ten(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));
      return 1;
    }
  if (klicu<5) 
    {
      send_message_to(this_player(),MT_NOTIFY|MT_NOISE,MA_USE,
        wrap("Cinkáš "+svuj(7)+". Zní to docela pěkně."));
      this_player()->send_message(MT_NOISE,MA_USE,
        wrap(Ten(1,this_player())+" cink"+plural("á ","ají ",this_player())
        +ten(7)+". Zní to docela pěkně."));
      return 1;
    }
  send_message_to(this_player(),MT_NOTIFY|MT_NOISE,MA_USE,
    wrap("Cinkáš "+svuj(7)+". No to je ale rámus!"));
  this_player()->send_message(MT_NOISE,MA_USE,
    wrap(Ten(1,this_player())+" cink"+plural("á ","ají ",this_player())
    +ten(7)+". No to je ale rámus!"));
  return 1;
}

Funkce init() a deklarace příkazů editovat

Známe už zvláštní roli funkce create(), která se volá automaticky bezprostředně po vytvoření objektu, a reset(), která se volá automaticky přibližně jednou za čtyřicet minut. Nyní k nim přidáme funkci init(), která se volá automaticky, když se s objektem setká živá bytost.

Setkání s živou bytostí znamená, že po pohybu objektu nebo živé bytosti se živá bytost ocitla v objektu (to je případ místností), že objekt se ocitl v živé bytosti (to je případ objektů, které hráč nosí s sebou), nebo že objekt a živá bytost se ocitli v témže jiném objektu (tedy například hráč přišel do místnosti, kde objekt leží).

Do funkce init() může mít tedy smysl vkládat herní akce, které se mají provést v okamžiku setkání se živou bytostí — například hostinský může mít ve funkci init() naprogramováno pozdravení příchozího — ovšem hlavně deklarace hráčských příkazů. Jakmile se živá bytost setká s objektem, definují se pro ni příkazy, které je objekt schopen zpracovávat (a posléze po vzdálení od objektu se tyto deklarace zase zruší).

K deklaraci hráčských příkazů slouží v našem příkladu funkce add_actions():[1]

void init()
{
  add_actions("cinkani",({"cinkej","cinkám","cinkam"}));
}

Prvním parametrem funkce add_actions() je jméno funkce, v níž je naprogramováno provedení příkazu, druhým parametrem je pole slov, jejichž zadáním může hráč příkaz spustit. V našem příkladu tedy hráč spustí deklarovaný příkaz větou, která začíná slovem "cinkej", "cinkám" nebo "cinkam", a program příkazu se bude hledat ve funkci cinkani().

Definiční funkce příkazu editovat

Funkce, v níž je naprogramováno samotné provedení příkazu, musí být typu int a mít jeden parametr typu string. To znamená, že funkci se předá řetězec, a ona vrátí celé číslo. Vrácené celé číslo má dokonce jen dvě smysluplné hodnoty — 1 pro případ, že příkaz se podařilo provést, a 0 pro případ, že to z nějakého důvodu nešlo:

int cinkani(string s)
{
  ...
  return 0; // zde skončí běh funkce, pokud se příkaz nepodařilo provést
  ...
  return 1; // zde skončí běh funkce, pokud se příkaz podařilo provést
  ...
}

Příkazem return se funkce ukončí, a jako její výsledná (návratová) hodnota se na místo jejího volání předá hodnota následujícího výrazu. Pokud je to 1, považuje se příkaz za provedený, a tím též za vyřízený. Pokud je to 0, hledá se další objekt, který definuje příkaz stejného znění, a pokud se takový nenajde, obdrží hráč ve funkci definovanou hlášku o selhání příkazu nebo záchytnou hlášku Prosím?[2]

Funkce notify_fail() editovat

Ve funkci cinkani() vidíme po úvodní deklaraci pomocné proměnné klicu tři příkazy if a return:

  int klicu;

  if (!s) 
    return notify_fail("Čím chceš cinkat?\n");
  if (!me(s,7)) 
    return notify_fail("Tím se nedá cinkat.\n");
  if (!(klicu=sizeof(all_inventory()))) 
    return notify_fail("Bez klíčů? Jak to chceš provést?\n");

Klíčové slovo if uvozuje podmíněný příkaz. Pokud je výraz následující v závorce nenulový, pak se následující příkaz (nebo vícero příkazů uzavřených do složených závorek) provede, pokud je výraz nulový, pak se příkaz neprovede.[3]

Přesný obsah výrazů si vysvětlíme níže. Nyní nám stačí předběžně vědět, že první znamená, že hráč za slovem, které spustilo příkaz (tedy „cinkej“, „cinkám“ nebo „cinkam“), nezadal už žádné jiné, takže nevíme, zda chtěl cinkat touto klíčenkou; druhý znamená, že následná slova zadaná hráčem neodkazují na tuto klíčenku, tedy hráč chce asi cinkat něčím jiným; třetí znamená, že tato klíčenka neobsahuje žádné klíče — to jsou různé překážky, které mohou způsobit, že hráčský příkaz cinkej nebude možno provést.

Nastane-li některý z popsaných tří případů, je tedy nutno vrátit hodnotu 0, ale také někam zaznamenat hlášku, která se má vydat v případě, že ani žádný další objekt nedokáže provést zadaný hráčský příkaz. A právě to obstarává funkce notify_fail().

Parametrem této funkce je hláška, kterou má — v případě, že se ani žádnému jinému objektu nepovede tento hráčský příkaz provést — obdržet hráč, jenž zadal zpracovávaný příkaz, jako odůvodnění, proč se příkaz neprovedl. Pokud tedy hráč zadal jen jedno slovo a nic dalšího, obdrží otázku: „Čím chceš cinkat?“ Pokud hráč zadal označení jiného objektu, obdrží hlášku: „Tím se nedá cinkat.“ Konečně, pokud klíčenka neobsahuje žádné klíče, obdrží hráč hlášku: „Bez klíčů? Jak to chceš provést?“[4]

Návratová hodnota funkce notify_fail() je vždy 0, což nám umožňuje používat onen zkrácený zápis, který vidíme v našem příkladu:

  if (!s) 
    return notify_fail("Čím chceš cinkat?\n");

Úplně stejný význam by měl o něco obsáhlejší zápis:

  if (!s) 
    {
      notify_fail("Čím chceš cinkat?\n");
      return 0;
    }

Analýza vstupu editovat

Jak jsme si řekli, první podmínka zjišťuje, zda po prvním slově, které vyvolalo funkci cinkani(), následují ještě nějaká slova další. Pokud ne, pak obdrží funkce cinkani() místo svého parametru nulovou hodnotu. Operátor ! znamená logickou negaci, tedy má-li s nulovou hodnotu, má !s hodnotu 1, a je-li naopak s nenulové (tedy nějaký řetězec zapsatelný do uvozovek), má !s hodnotu 0:

   if (!s)

Pokud je nám toto vysvětlení příliš nepřehledné, můžeme si danou podmínku přeložit jako:

   pokud (není řetězec)

Druhá podmínka je složitější. Rozpoznáváme v ní operátor !, za ním ovšem následuje volání dosud nám neznámé funkce me():

  if (!me(s,7))

Funkce me() je jednou z několika funkcí umožňujících syntaktickou analýzu hráčského vstupu, tedy rozpoznání, která slova označují objekty, s nimiž se má něco provést, která jsou jmény zúčastněných osob, která slouží jen jako spojky či předložky atd.[5] Funkce me() konkrétně zjišťuje, zda se na začátku daného řetězce nachází pojmenování objektu, v jehož programu se právě nacházíme, a to ve tvaru odpovídajícím danému pádu. Pokud ano, vrátí se zbytek tohoto řetězce (což může být i "", když řetězec obsahoval právě označení tohoto objektu a nic jiného), pokud ne, vrátí se 0.

V našem příkladě se tedy ptáme, zda začátek řetězce s popisuje klíčenku v 7. pádě. Předpokládejme, že vlastnosti klíčenky jsou nastaveny tak, že její základní označení je „kovový kroužek na klíče“ a doplňkovým identifikátorem je „klíčenka“. Jak pracuje funkce me(s,7), si můžeme představit z následující tabulky:

zadaný příkaz funkci předaný řetězec s hodnota me(s,7)
"cinkej" 0 (funkce skončí už dříve)
"cinkej kroužek" "kroužek" 0
"cinkám kroužkem" "kroužkem" ""
"cinkám kovovym kroužkem" "kovovym kroužkem" ""
"cinkam kovovym krouzkem na klice" "kovovym krouzkem na klice" ""
"cinkám kovovým kroužkem, který visí na klice" "kovovým kroužkem, který visí na klice" "který visí na klice"
"cinkej dřevěným kroužkem" "dřevěným kroužkem" 0
"cinkej kovovou klicenkou" "kovovou klicenkou" ""
"cinkej kovovým klíčenkou" "kovovým klíčenkou" 0
"cinkej klíčenkou na pytel" "klíčenkou na pytel" "na pytel"

Tabulku bychom mohli prodlužovat do nekonečna, resp. do překročení maximální délky vstupu, a obdobné tabulky bychom si mohli sestavit pro případ, že by měla klíčenka nastavené jiné přívlastky a vedlejší identifikátory. Hlavní výhodou funkce me() (a dalších funkcí pro syntaktickou analýzu) je, že pro každý řetězec z tohoto nesmírného množství možných vstupů a nastavení objektu dokáže posoudit, zda řetězec odpovídá objektu, a odpovědět buď „ano“ (tedy zbytek řetězce), nebo „ne“ (tedy 0). V našem příkladě pak „ano“ znamená, že program postoupí o krok dále, a „ne“ znamená, že se jako hláška o neúspěchu příkazu nastaví „Tím se nedá cinkat.“ a funkce cinkani() se ukončí s návratovou hodnotou 0.

Několik drobnůstek pro běžnou práci editovat

Třetí podmínka obsahuje hned několik novinek, které si musíme vysvětlit, ale které se nám budou hodit velmi často:

  if (!(klicu=sizeof(all_inventory())))

Základem podmínkového výrazu je funkce all_inventory(). Je-li zavolána takto bez parametru, pak vrací úplný inventář tohoto objektu, tedy pole všech objektů, které se nacházejí v tomto objektu.[6]

Pole vrácené funkcí all_inventory() se v našem příkladě předává funkci sizeof(). To je funkce, jejímž jediným parametrem je pole (též asociativní pole), a jejíž návratovou hodnotou je celé číslo, udávající počet prvků tohoto pole. V našem příkladě tedy sizeof(all_inventory()) vrací počet objektů, které se nacházejí v objektu klíčenky — z jiného nastavení klíčenky ovšem plyne, že do objektu klíčenky se dají přesouvat pouze klíče, tedy počet objektů přítomných v objektu klíčenky znamená počet na ní zavěšených klíčů.

Operátor = znamená přiřazení hodnoty výrazu uvedeného vpravo od tohoto operátoru proměnné uvedené vlevo od něho. V našem příkladě uvedené klicu=sizeof(all_inventory()) tedy můžeme do lidského jazyka přeložit jako „přiřaď proměnné klicu počet prvků inventáře tohoto objektu“. Důležité je si uvědomit, že tento výraz má jako celek opět přiřazovanou hodnotu — následně toho hned využíváme, protože se ptáme, zda je tato hodnota nulová (tedy v tomto objektu se nenacházejí žádné jiné objekty, tj. na klíčence nejsou žádné klíče) — či přesněji nejprve pomocí negačního operátoru ! uděláme z případné nulové hodnoty nenulovou a z nenulové nulovou, a pak se pomocí if ptáme, zda tento výsledek je nenulový.

Podmínková struktura a porovnávací operátory editovat

Po průchodu úvodními třemi podmínkami, které prověřují, zda se klíčenkou dá skutečně zacinkat, máme před sebou samotný program cinkání. Všimneme si nejprve jeho celkové struktury:

  if (klicu==1) 
    {
      ...
      return 1;
    }
  if (klicu<5) 
    {
      ...
      return 1;
    }
  ...
  return 1;
}

Znamená to, že nejprve se otestuje, zda je hodnota proměnné klicu rovna 1; pokud ano, pak se provede první část kódu a vykonávání funkce se úspěšně ukončí (vrácená hodnota 1 znamená, že hráčský příkaz cinkej je tímto vyřízen a není nutno hledat jiné cinkací objekty). V opačném případě se otestuje, zda je hodnota proměnné klicu menší než 5; pokud ano, provede se druhá část kódu a funkce se úspěšně ukončí. V opačném případě se pak už netestuje nic, ale provede se třetí část kódu a funkce se úspěšně ukončí.

První podmínka porovnává hodnotu celočíselné proměnné klicu s celým číslem 1 (pro porovnávání musí být obě veličiny stejného typu) pomocí operátoru ==. Výsledek je 1, pokud jsou si porovnávané výrazy rovny, a 0, pokud nejsou; v prvním případě je pak podmínka splněna a vykoná se následující blok příkazů.

Porovnávací operátor == nesmíme zaměňovat s přiřazovacím operátorem =. Kdybychom za if jako podmínku napsali (kolik=1), přiřadili bychom tím hodnotu 1 proměnné kolik; přiřazení jako celek by pak mělo vždy hodnotu 1, tedy vždy by se provedl první blok kódu, bez ohledu na množství klíčů na klíčence.

Porovnávací operátor < u další podmínky funguje obdobně: má výsledek 1 nebo 0 podle toho, zda je pravda, že kolik je menší než 5. Stejně bychom mohli použít i další porovnávací operátory (>, <=, >=, !=). Protože jejich výsledkem je logické „ano“ nebo „ne“ (tedy 1 nebo 0), je použití doplňkového operátoru zcela ekvivalentní kombinaci s negačním operátorem !, tedy třeba !(kolik==4) funguje úplně stejně jako kolik!=.

Gramatické funkce editovat

Ze tří programových bloků, které popisují samotný výkon hráčského příkazu cinkej, nám bude stačit probrat první — oba následující už využívají stejných funkcí a syntaktických konstrukcí, takže jim po pochopení prvního rovněž beze zbytku porozumíme:

      send_message_to(this_player(),MT_NOTIFY|MT_NOISE,MA_USE,
        wrap("Cinkáš "+svuj(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));
      this_player()->send_message(MT_NOISE,MA_USE,
        wrap(Ten(1,this_player())+" cink"+plural("á ","ají ",this_player())
        +ten(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));
      return 1;

Jak vidíme, blok sestává (pomineme-li teď závěrečný příkaz k návratu) ze dvou funkčních volání, kterými se zajišťuje poslání hlášky hráči, který cinká, a hráčům ostatním. Než si uvědomíme, jak funguje toto posílání hlášek, pohlédněme na samotné hlášky a jejich strukturu. Vidíme řetězce a funkce pospojované operátorem + a na závěr zpracované funkcí wrap().

Funkce this_player(), kterou v textu programu několikrát vidíme, odkazuje na hráče (nebo i nehráčskou bytost), jehož hráčský příkaz právě provádíme (obdobně jako již dříve potkané this_object() odkazuje na objekt, v jehož programu se právě nacházíme).

Všechny další uvnitř hlášek použité funkce (svuj(), nej(), Ten(), plural(), ten()) patří mezi funkce gramatické, které jsou schopny vyrobit správně vyskloňovaný nebo vyčasovaný tvar slova podle gramatických údajů objektu (tedy podle nastaveného rodu, čísla a vzoru). Pokud v těchto funkcích není zadán objekt, pak se pracuje s objektem, v jehož programu se právě nacházíme. Pokud je tedy klíčenka nastavena jako „kovový kroužek na klíče“, vrátí funkce svuj(7) řetězec "svým kovovým kroužkem na klíče", funkce nej(6) vrátí "něm", funkce ten(7) vrátí "kovovým kroužkem na klíče". Funkce pojmenované velkým písmenem vrací řetězec začínající velkým písmenem, tedy je-li hráč třeba „skrz naskrz promočený Michael“, vrátí Ten(1,this_player()) řetězec "Skrz naskrz promočený Michael". Funkce plural() vybírá ze dvou zadaných řetězců — první použije v případě, že daný objekt je v jednotném čísle, druhý v případě, že daný objekt je v čísle množném.[7]

Gramatické funkce vracejí řetězcovou hodnotu, a operátor + prostě slepí přímo vypsané i z funkcí vrácené řetězce v daném pořadí do jednoho dlouhého řetězce. Funkce wrap() pak výsledný řetězec zaláme do řádek a na konec přidá rovněž jedno odřádkování.

Posílání hlášek editovat

Nyní pohlédneme na samotné posílání hlášek. V našem příkladě vidíme použity obě základní funkce, které ke posílání hlášek slouží: funkci send_message_to(), která pošle hlášku určitému objektu, a funkci send_message(), která rozešle hlášku do okolí určitého objektu:

      send_message_to(this_player(),MT_NOTIFY|MT_NOISE,MA_USE,
        wrap("Cinkáš "+svuj(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));
      this_player()->send_message(MT_NOISE,MA_USE,
        wrap(Ten(1,this_player())+" cink"+plural("á ","ají ",this_player())
        +ten(7)+", ovšem vzhledem k tomu, že na "
        +nej(6)+" visí jen jeden klíč, zní to dost uboze."));

Podrobný popis obou funkcí najdeme v encyklopedii. Nyní si všimněme především toho, jakým způsobem tyto funkce voláme. Funkci send_message_to() voláme přímo v tomto objektu, a cílový objekt uvádíme jako první parametr — vlastně je jedno, odkud hlášku pošleme, výsledkem prostě bude, že daný objekt (zde hráč, jehož příkaz právě vykonáváme) obdrží příslušnou hlášku. Funkci send_message() oproti tomu musíme zavolat v objektu, do jehož okolí se má hláška rozšířit, používáme proto konstrukci call_other(), s níž jsme se seznámili v minulé lekci.

Konstanty začínající na MT_ a MA_ popisují, jakým způsobem se hláška přenáší (MT_NOISE kupříkladu znamená, že zvukem) a z jaké akce pochází (MA_USE znamená, že použitím nějakého předmětu). Tyto parametry umožňují reagovat na různá specifika hráčova smyslového vnímání (hráč s klapkami na uších neuslyší cinkání, hráč pod vlivem drog uvidí své okolí zdeformovaně atd.) nebo probíhajících akcí (hlášky vzniklé pohybem mohu například spustit fotobuňku atd.). Operátorem | můžeme tyto konstanty skládat, pokud pro nějakou hlášku platí více možností zároveň (například k MT_NOISE přidané MT_NOTIFY znamená, že hráč hlášku dostane jako zprávu i v případě, že by měl klapky na uších, protože přece ví, že cinká klíči).

Abychom mohli tyto konstanty používat, musíme nejprve v programovém souboru uvést řádku, která potřebné definice načte ze standardního hlavičkového souboru:

#include <message.h>

Přehled použitelných konstant můžeme vyvolat encyklopedickými příkazy ? MT_LIST a ? MA_LIST.

Úkoly do příští lekce editovat

Vyberme jeden z objektů, které jsme dosud vyrobili (může to být některá z místností, ale také hlavní objekt, na kterém pracujeme), a opatřme ho nějakým hráčským příkazem. Otestujme, zda skutečně pracuje tak, jak by měl, a případně ho podle potřeby opravme. Pokud si s něčím nevíme rady, můžeme využít stránku domácího úkolu ke konzultaci, nebo si zkusit okoukat některé postupy z programů složitějších hráčských příkazů, které najdeme v programových knihovnách mudu.

Poznámky editovat

  1. Funkce add_actions() je specifická pro programovou knihovnu mudu Prahy. Je odvozena z obecnější, avšak jen na jednu podobu příkazu omezené externí funkce add_action(). Pro deklarace příkazů v češtině, která zpravidla může týž příkaz vyjádřit různými slovy, je většinou pohodlnější funkce add_actions(), která umožňuje zadání celého pole možných příkazů.
  2. Forma Prosím? samozřejmě také nesouvisí se samotným LPC, nýbrž je specifická jen pro Prahy, ba i v Prazích by mohla být předefinována.
  3. Příkaz if může mít ještě část else, která se provádí naopak tehdy a jen tehdy, pokud je úvodní výraz nulový. V dalších lekcích se k tomu ještě podrobněji vrátíme.
  4. Můžeme si povšimnout již dříve vysvětleného zvláštního znaku \n na konci řetězců předaných funkci notify_fail(). Funkce totiž předané hlášky nijak neformátuje — v některých případech může být smysluplné, aby se po hlášce neodřádkovávalo — proto musíme odřádkování dodat takto „ručně“.
  5. Základní funkcí pro tuto analýzu je parse_com(), kterou si ovšem pro její složitost necháme na později.
  6. Zápis all_inventory() je vlastně zkratka za all_inventory(this_object()). Můžeme jí tedy zjišťovat objekty obsažené v libovolném objektu — pokud by třeba objektová proměnná prod znamenala prodavače a obch jeho prodejnu, vrátilo by volání all_inventory(prod) prodavačův inventář, tedy pole objektů, které má jako nehráčská postava u sebe, a all_inventory(obch) inventář prodejny, tedy pole objektů nacházejících se v ní, včetně prodavače a dalších přítomných bytostí.
  7. Gramatických funkcí je mnohem více, než vidíme v tomto příkladu. Nejlepší bude, když si je postupně probereme podle encyklopedie. Začneme funkcí ten() (tedy zadáme encyklopedický příkaz ? ten), pak si probereme ostatní zde použité, a budeme pokračovat po funkcích uvedených v sekci VIZ: v popisu jednotlivých funkcí.

Pomocné stránky editovat