Pojďte si s námi naprogramovat hru!

Povedeme vás krok za krokem a naprogramujete si od základu celou funkční hru Hadi a žebříky! Při tom se naučíte se základní principy objektově orientovaného programování, jak ho učíme na FIT a také se naučíte prakticky celý jazyk Smalltalk, který je (jak sami uvidíte) velmi jednoduchý a současně mocný jazyk.

Hra Hadi a žebříky

"Hadi a žebříky", neboli v angličtině "Snakes and Ladders" je stará indická desková hra pro dva a více hráčů. Hrací deska obsahuje očíslovaná políčka. Na desce je přítomen také určitý počet "hadů" a "žebříků", které spojují určitá políčka.

Hráči se střídají na tahu. V každém tahu hráč hodí kostkou a posunuje se o hozený počet políček vpřed. Cílem je dorazit co nejrychleji na poslední políčko. Kdo dorazí, tak vyhrál.

Postup po desce je ovšem "okořeněn" zmíněnými žebříky, které hráče posunují vpřed o určitý počet políček a hady, které naopak hráče posunují zpět o určitý počet políček.

Hru, kterou spolu naprogramujeme si můžete pro motivaci zahrát v krásné graficky původní podobě například zde:

Hrát hru

Pro koho je náš tutoriál?

  • Pokud nemáte žádné zkušenosti s programováním, ale zajímá vás, pak jste na správné stránce: zábavnou formou se naučíte základy objektově-orientovaného programování a dokonce i většinu jazyka Smalltalk! Třeba vás to inspiruje naučit se víc a začít programovat vlastní aplikace!

  • Pokud již máte zkušenosti s programováním, můžete se podívat, jak elegantně lze naprogramovat hra s použitím čistě objektově-orientovaného přístupu. Možná najdete inspirace do svého oblíbeného programovacího jazyka, nebo vás dokonce zaujme Smalltalk. To bychom byli rádi, protože věříme, že se jedná o jazyk, který stojí za to!

V čem budeme programovat?

Čisté objektově-orientované paradigma

Nyní si krátce představíme technologie, se kterými budeme pracovat. Pokud jste nedočkaví, můžete rovnou přeskočit na programování a tyhle zajímavosti si přečít někdy později.

Způsob, jakým se programuje se odborně nazývá paradigma, česky cosi jako "myšlenovké schéma, způsob uvažování". My zde budeme používat čisté objektové paradigma, které na programovaný systém nahlíží jako na soubor inteligentních, samostatných objektů, které spolu navzájem komunikují zasíláním zpráv. Na zaslání zprávy objekt typicky zareaguje vyvoláním příslušné metody. Může se ale také stát, že objekt zprávě nebude rozumět a v tom případě nastane tzv. výjimka. Tento způsob uvažování o programu je zvláště vhodný v případech, kdy programujeme chování nějakého systému z naší reality (firemní systém, hra, apod.).

image_sender.png, 95kB

Naprostá většina dnešních moderních programovacích jazyků má v sobě prvky objektového paradigma. Můžeme jmenovat např. C++, Javu, C#, Ruby, Python, PHP, ... a řada dalších. Tyto jazyky však z různých důvodů nejsou čistě objektově orientované. Abychom totiž toto mohli prohlásit, musí platit, že všechno je objekt a nic než objekt a celý život programu spočívá pouze v zasílání zpráv mezi objekty.

Trocha historie

Historie objektově-orientovaného paradigma se datuje do 70. let 20. století, kdy Alan Kay se svou výzkumnou skupinou pracovali v Xerox PARC (Palo Alto Research Center) na vývoji způsobu programování škálovatelných systémů, tj. systémů, které jsou snadno rozšiřovatelné a mohou "růst" bez větších problémů, které (dodnes!) ochromují systémy, když se rozrostou z malé aplikace na velký systém. Alan Kay byl původním zaměřením biolog a fascinovalo ho, jak živé systémy umí dokonale škálovat: maličká buňka je schopna perfektně autonomně fungovat a stejně perfektně funguje i velký organismus složený z obrovského počtu buněk.

To přivedlo Alana Kaye a jeho tým na myšlenku napodobit živé systémy a začali budovat softwarové systémy s podobnou filosofií. Základem byl objekt, který oproti "číslu" či "řetězci" a jiným typickým "objektům" v paměti počítače je inteligentní: umí reagovat na vnější podněty. Své chování vykonává na základě své vnitřní struktury, kde má uložen svůj stav.

image_video_01.png Výzkumný tým v Xerox PARC začal svůj nápad realizovat a začali stavět dokonce celé počítače založené na tomto principu. Byly to fascinující roky plné objevů a v Xeroxu spatřilo světlo světa řada "vynálezů", které dnes považujeme za samozřejmé:

  • ovládání myší a tabletem,
  • grafické uživatelské rozhraní s okýnky a ikonami,
  • WYSIWYG textový editor,
  • ethernet,
  • integrované vývojové prostředí (IDE).

A snad nejrevolučnější myšlenka byl vývoj osobního počítače nazvaného "Dynabook" -- v 70. letech byste si totiž typický počítač do klína rozhodně nedali: typicky zabíral velkou místnost. Vizionáři v Xeroxu již však v té době pracovali na prototypu malého osobního počítače a jejich ideou bylo, aby s počítačem mohl zacházet každý, dokonce i dítě.

image_video_02.png, 46kB Programovacím jazykem, který vyvinuli pro "povídání si s objekty" byl právě Smalltalk. Díky své geniálně jednoduché syntaxi nebylo dodnes třeba jej měnit ani rozšiřovat. (Jazyk byl dokončen v r. 1984 a je nazván Smalltalk-80. Současná norma ANSI Smalltalk pocházi z r. 1998). Přitom dnes v moderních implementacích Smalltalku je možné naprogramovat cokoliv, na co jste zvyklí z ostatních programovacích jazyků, a to téměř vždy rychleji a elegantněji. Jak je to možné? Všechna "složitost" používání síťových protokolů, databází, uživatelských rozhraní, webových aplikací, atd. je vybudována z "chytrých objektů", které si všechnu složitost "pořeší mezi sebou" a programátor jim jen říká, co chce, aby dělaly (zasílá zprávy). Není to ale samozřejmě úplně "jen tak" -- je třeba umět "objektově myslet" a znát pasti, do kterých lze spadnout. Jde to ale snadno, protože se jedná o přirozené věci, které známe ze světa okolo sebe -- však uvidíte až začnete programovat Hady a žebříky, tato malá hra vás naučí hodně z tajů objektové kuchyně.

Vraťme se ale ještě na chvíli do Xerox PARC. Moc už nám toho nezbývá povyprávět, úžasné věci, které tam v 70. letech začaly vznikat totiž skončily z politicko-ekonomických důvodů začátkem 80. let. Řada myšlenek naštěstí našla svoje pokračování jinde -- nejvíce ve spřátelené firmě Apple, kam odešel Alan Kay a která převzala a rozvinula ovládání pomocí myši, grafické uživatelské rozhraní, grafický textový editor a další vynálezy z Xerox PARC. Smalltalk též naštěstí nezanikl. V současné době máme k dispozici komerční verze implementací Smalltalk, např. skvělý VisualWorks firmy Cincom. K dispozici jsou i open-source implementace, tady vede asi projekt Pharo. Nesmíme ale zapomenout ani na velmi zajímavý (byť z konvenčního pohledu trochu netypický) vizionářský Squeak, který pochází od samotného Alana Kaye.

Programování s "živými" objekty

Programování ve Smalltalku se někdy nazývá též programování s živými objekty, nebo programování "za běhu". Co je pro něj typické a čím se odlišuje od konvenčního paradigma tzv. imperativního programování (dále "IP")?

  • Neexistuje cosi jako "hlavní" program. Programujeme pouze chování jednotlivých objektů, logika programu je dána pouze jejich vzájemnou komunikací -- oproti tomu imperativní program je pojat jako sekvence příkazů prováděných odshora dolů.
  • Kód má podobu krátkých, dobře čitelných metod. Metody mají typicky pouze několik řádků a je snadné letmým nahlédnutím poznat cíl a způsob fungování metody.
  • Tyto metody jsou organizovány do kategorií metod, jakýchsi přehledných "přihrádek" v každém objektu, které nám umožní se snadno a rychle pohybovat v kódu i velkých systémů pomocí nástroje "Object browser" -- oproti tomu v IP jsme nuceni se probírat výpisy kódu v souborech častokrát majících stovky i tisíce řádků.
  • Programování za běhu. Toto považuji osobně za nejúžasnější vlastnost. Celý objektový systém totiž od okamžiku spuštění vývojového prostředí "žije" a programování je vlastně "dodělávání" systému formou vytváření dalších objektů, které interagují s již běžícími objekty vývojového prostředí a jsou ihned k dispozici. To umožňuje velmi zábavné a interaktivní programování. Je asi zbytečné tady o tom dlouze psát, nejdůležitější je zkušenost -- během programování Hadů a žebříků ji prožijete! Pro připomenutí: v IP nás při každé změně kódu čeká nová kompilace a spuštění programu. Kompilace i spuštění dnes bývá na moderních počítačích velmi rychlé, ovšem objeví se vám opět čerstvě spuštěný program a musíte se -- často velmi zdlouhavě -- dostávat do místa, kde jste program opustili.
  • Vývojové prostředí + vyvíjený program = virtuální počítač. Toto souvisí s předchozím bodem: kdykoliv můžete uložit stav celého vývojového prostředí, včetně spuštěné aplikace či aplikací na disk. Při příštím spuštění se poté obnoví původní stav. Jedná se o situaci stejnou jako např. při používání tzv. virtuálního počítače (např. VirtualBox či VMware). Technicky se systém skládá z tzv. image (obrazu), který obsahuje komplet vývojové prostředí včetně uživatelských objektů a spuštěných aplikací v běžícím stavu a tzv. virtuální mašiny (virtual machine), která umí image spouštět a interpretovat. Podobné řešení převzala Java, ovšem zde je v image pouze "mrtvá" aplikace, bez živého vývojového prostředí. Tady se nováčci často ptají, jestli je ale možné potom mít ve Smalltalku i program jako jednoduchou spustitelnou aplikaci, tj. již bez vývojového prostředí. Odpověď je samozřejmě ano: lze vyrobit image, který pouze spouští naprogramovanou aplikaci a vývojové prostředí je z něj odstraněno.

Pojďme na to!

Připravte si "nářadí"

Abyste mohli začít hru programovat, stáhněte si vývojové prostředí Pharo:

Vývojové prostředí Pharo

Vpravo nahoře na stránce se nachází "Download Pharo". Jedná se o tzv. "one-click" image, který spustíte na Windows, Linux i MacOS. Na MacOS se chová přímo jako aplikace (.app), ve Windows a Linuxu se jedná o adresář, ve kterém jsou skripty na spuštění: Pharo-1.4-one-click.bat pro Windows a Pharo-1.4-one-click.sh pro Linux. Ve vývojovém prostředí je již vše potřebné, abyste mohli začít programovat!

Jak tutoriál nejlépe absolvovat?

  1. Nejdříve doporučujeme nečíst úplně detailně všechny texty, ale spíš si pouštět jednotlivá videa a dívat se na ukázky. To vám dá ucelený obrázek, jak hru naprogramovat a jak ovládat prostředí Pharo. V tuto chvíli ještě nemusíte všemu rozumět.
  2. Poté projeďte tutoriál znovu od začátku a čtěte texty. Zatím ale nerozlikávejte otazníky u kódu. To vám dá hlubší pochopení, co a proč děláme.
  3. No a nakonec projděte tutoriál do třetice s rozklikáváním otazníků u kódu, kdy detailně pochopíte, co který řádek v kódu dělá.

Pro zorientování najdete v levém pruhu navíc odkazy na stránky s různými přehledy.

Pár slov na úvod

Řešení, které zde bude prezentováno bylo laskavě poskytnuto Prof. Oscarem Nierstraszem ze Software Composition Group University of Berne. Pro potřeby tohoto tutoriálu bylo lehce zjednodušeno.

Abychom se zbytečně nezapletli do složitostí, dovolíme si drobné "prohřešky" proti doporučovaným praktikám (best practices) objektového programování, např. budeme přistupovat k instančním proměnným uvnitř objektů přímo a nikoliv přes přístupové metody a též nebudeme příliš hledět na kontrolu chybových stavů a psaní testů. Pro dobrou čitelnost kódu česky mluvícím čtenářem budeme psát kód česky a zachováme pouze anglická jména systémových knihoven (byť by technicky nebyl problém je "obalit" česky pojmenovanými objekty).

1. Kostka

kostka

Začněme něčím jednoduchým: vyrobíme si hrací kostku, tedy třídu Kostka. Od kostky budeme chtít jediné chování: hod kostkou, tedy metodu hod. Tuto metodu umístíme do kategorie metod hrani:

Browser -> Kostka -> hrani:

hod
  ^ (1 to: 6) atRandom
Tento výraz říká: "číslu 1 pošli zprávu #to: (do)", což vygeneruje řadu čísel 1 až 6. Této řadě pošleme zprávu #atRandom, čímž říkáme, že chceme jedno náhodné číslo z řady. Stříška říká, že tento výraz se má vrátit jako výsledek metody.

Nyní můžeme kostku vyzkoušet. K tomu si otevřeme Workspace, do kterého napíšeme:

Workspace:

Kostka new. -->Inspect
Vytvoříme novou instanci (objekt) třídy Kostka,tím, že třídě Kostka pošleme zprávu new. Poté vyvoláme Inspector na novou instanci z kontextového menu pravého tlačítka myši. V Inspectoru si můžeme prohlédnout celou strukturu objektu (instanční proměnné) a zprávy, kterým rozumí. Instanční proměnné můžeme dokonce přímo nastavovat a můžeme též objektu zasílat zprávy -- vše je "živé", programujeme s Live Objects.

self hod.
VIDEO 01

2. Hra

hra

Nyní se pustíme do programování vlastní hry! Vytvoříme tedy třídu (class) Hra, která bude reprezentovat naši hru. Z této třídy poté bude možné generovat jednotlivé instance (instances), tedy konkrétní objekty hry, které budou mít strukturu a chování definovanou právě třídou, ale každý objekt bude mít svoje hodnoty pro instanční proměnné (instance variables), tedy proměnné (datové prvky).

Hru můžeme charakterizovat těmito instančními proměnnými:

  • políčka,
  • hráči,
  • kostka,
  • kdo je na tahu,
  • indikace, že hra je dohrána.

Typická hrací deska hry je dvojrozměrná, nicméně pokud se na ni podíváme pečlivě, vidíme, že políčka jsou na ní uspořádána lineárně -- jedná se tedy o "šňůru" políček zaklikacenou na plochu dvojrozměrné desky. Můžeme tedy pro účely tohoto tutoriálu pro jednoduchost bez jakékoliv újmy uvažovat hrací desku pouze jako posloupost (onu šňůru) políček, tj. jednorozměrnou strukturu.

Vytvoříme tedy třídu Hra s instančními proměnnými a také vyrobíme metodu #initialize. Tato metoda je automaticky volána systémem po vytvoření instance objektu, slouží tedy jako tzv. konstruktor (constructor), ve kterém můžeme zinicializovat počáteční stav objektu:

Browser -> Hra -> inicializace:

initialize
  kostka := Kostka new.
Vytvoříme novou instanci (objekt) třídy Kostka,tím, že třídě Kostka pošleme zprávu new. Tu poté přiřadíme do instanční proměnné kostka pomocí přiřazení (assignment) ":=". Výraz je ukončen tečkou (podobně jako v jazycích C, C++, C#, Java, apod. je středník).

  policka := OrderedCollection new.
Podobně vytvoříme novou instanci (objekt) třídy OrderedCollection, do kterého budeme vkládat políčka hrací desky. O kolekcích si více povíme za chvíli ;-).

  hraci := OrderedCollection new.
  naTahu := 1.
Na tahu bude na začátku hráč č. 1.

  dohrano := false.
Na začátku dohráno zcela určitě není, tedy proměnná dohrano bude obsahovat pravdivostní hodnotu false, tedy "nepravda" (opakem je true, tedy "pravda").

Poté vyrobíme třídní metodu (class method), example, která nám vytvoří příklad hrací desky. Třídní metoda je metoda, která je definována na úrovni třídy. Opakem k tomu je instanční metoda (instance method), která definuje chování odlišné pro každou instanci.

Browser -> Hra Class -> instanciace:

ukazkovaHra
  | hra |
Definujeme lokální (dočasnou, temporary) proměnnou. Její platnost je pouze v rámci této metody.

  hra := self new.
Vytvoříme novou instanci (objekt) sebe, tedy hry tím, že sobě (tedy třídě Hra) pošleme zprávu new, která nám vytvoří novou instanci (objekt) hry.

  hra
    pridejPolicko: Policko new;
A této instanci pošleme zprávu #pridejPolicko, kterou přidáme políčko na hrací desku. Středník na konci řádku říká, že budeme hned posílat tomu stejnému objektu další zprávu -- tomu se říká kaskáda:

    pridejPolicko: (PolickoZebrik new nastavDopredu: 4);
Přidáme políčko s žebříkem, které bude posunovat o 4 dopředu. Třídě PolickoZebrik pošleme zprávu #new, která nám vrátí novou instanci (objekt) políčka a tomuto objektu hned posíláme zprávu #nastavDopredu:, kterou jej nastavíme.

    pridejPolicko: Policko new;
    pridejPolicko: Policko new;
    pridejPolicko: Policko new;
    pridejPolicko: Policko new;
    pridejPolicko: (PolickoZebrik new nastavDopredu: 2);
    pridejPolicko: Policko new;
    pridejPolicko: Policko new;
    pridejPolicko: Policko new;
    pridejPolicko: (PolickoHad new nastavDozadu: 6);
Přidáme políčko s hadem, které vrací o 6 zpět.

    pridejPolicko: Policko new;
    novyHrac: (Hrac new nastavJmeno: 'Jan');
Přidáme hráče se jménem 'Jan'.

    novyHrac: (Hrac new nastavJmeno: 'Eva').
  ^ hra
Stříška říká, co se má vrátit jako výsledek provedení metody. V našem případě je to objekt hra. Za posledním výrazem v metodě tečku dělat nemusíme.

VIDEO 02

Pokud jste postupovali podle videa, máte nyní vytvořenu třídu Hra a též třídy reprezentující políčka: Policko, PolickoHad a PolickoZebrik.

3. Políčka

políčka

Políčko hrací desky je charakterizováno těmito údaji:

  • pozice políčka
  • hráči, kteří jsou přítomni na políčku
  • hra, ke které políčko patří.

Význam poslední položky (hra) je takový, že políčko bude potřebovat vědět, které políčko je předchozí a které následující, což jsou informace, které má právě Hra, která v sobě obsahuje všechna políčka. Do třídy Policko tedy přidáme vyjmenované instanční proměnné.

Instancni promenna hraci by měla mít možnost uložit vícero hračů, kteří mohou být na daném políčku. Pro takové účely používáme kolekce, což jsou "kontejnery", které umožňují uložit obecně libovolné množství jiných objektů. Kolekcí existuje ve Smalltalku řada typů lišících se svými vlastnostmi, my použijeme OrderedCollection, což je uspořádaná kolekce (každý prvek tedy má své pořadové číslo). Základní operace, které budeme od kolekce potřebovat jsou:

  • Přidání prvku do kolekce (nakonec): zpráva #add:
  • Získání prvku z kolekce na určité pozici: #at:
  • Odstranění prvku z kolekce: #remove:
  • Zjištění velikosti kolekce: #size

O velikost kolekce se starat nemusíme, ta roste sama s přidávanými prvky. Na začátku ale musíme kolekci v instanční proměnné hraci vytvořit. K tomu nadefinujeme opět metodu initialize:

Browser -> Policko -> inicializace:

initialize
  hraci := OrderedCollection new.

Po inicializaci budeme chtít nastavit další instanční proměnné, tedy hra a pozice:

Browser -> Policko -> inicializace:

nastavHru: aHra
Zde se jedná o metodu, která bude přijímat jeden vstupní parametr, který jsme si označili jako aHra. Ve Smalltalku je zvyk pojmenovávat vstupní parametry ve stylu "aTypObjektu", což v angličtině znamená "nějaký TypObjektu", čímž specifikujeme, jaký typ objektu očekáváme -- Smalltak je plně dynamický jazyk, typy tedy explicitně nedeklarujeme.

  hra := aHra.

nastavPozici: aNumber
  pozice := aNumber.

Nyní se podíváme na políčka s hadem a žebříkem. Ta by se měla chovat jako normální políčka s tím, že jejich specialita je navíc posun hráče dozadu resp. dopředu o určitý počet políček. K tomu využijeme techniky dědění (inheritance). Tato technika nám umožňuje, aby speciální typy tříd dědily od obecnějších typů strukturu (instanční proměnné) a chování (metody) a přidávaly další, speciální, strukturu (instanční proměnné) a chování (metody).

Třídy PolickoHad a PolickoZebrik tedy uděláme podtřídami třídy Policko a přidáme jim potřebné instanční atributy, ve kterých si budou držet údaj o kolik políček posunují. Na závěr přidáme metody pro inicializaci těchto instančních proměnných:

Browser -> PolickoHad -> inicializace:

nastavDozadu: oKolik
  dozadu := oKolik.
Browser -> PolickoZebrik -> inicializace:

nastavDopredu: oKolik
  dopredu := oKolik.

Metoda #pridejPolicko:

Nyní můžeme ve třídě Hra implementovat metodu #pridejPolicko:

Browser -> Hra -> inicializace:

pridejPolicko: aPolicko
  policka add: aPolicko.
Do kolekce políček reprezentujících hrací desku přidáme políčko použitím zprávy #add:.

  aPolicko
Políčko nyní zinicializujeme zasláním kaskády zpráv.

    nastavHru: self;
    nastavPozici: policka size
Pozice nového políčka je na konci, což odpovídá aktuální velikosti hrací plochy: kolekci policka zašleme zprávu size

VIDEO 03

4. Hráč

hráč

Metoda #novyHrac:

Pro naprogramování metody potřebujeme vytvořit třídu Hrac. Hráč bude mít jméno a bude setrvávat na nějakém políčku, budeme tedy potřebovat instanční proměnné jmeno a policko. Pro inicializaci jména vytvoříme metodu #nastavJmeno::

Browser -> Hrac -> inicializace:

nastavJmeno: aString
Jméno bude typu řetězec, tedy anglicky String.

  jmeno := aString

Nyní naprogramujeme metodu #novyHrac:

Browser -> Hra -> inicializace:

novyHrac: aHrac
  hraci add: aHrac.
  aHrac presunNa: self prvniPolicko.
Přidaného hráče chceme mít na prvním políčku.

V metodě voláme vlastní metodu #prvniPolicko, která nám vrátí první políčko z hrací desky. Tuto metodu umístíme do kategorie testovani:

Browser -> Hra -> testovani:

prvniPolicko
  ^ policka at: 1
Políčko jednoduše vrátíme zprávou #at: zaslané kolekci policka. Jedná se o velmi jednoduchý kód, který by technicky bylo možné v metodě #novyHrac: napsat přímo. V objektovém systému se však snažíme, aby kód "hovořil" a každá metoda byla co nejmenší a nejsrozumitelnější. Systém (program) se poté skládá z mnoha malých metod, což je stav, který je blahodárný pro rozšiřitelnost a budoucí úpravy systému.

Ještě nám zbývá naprogramovat metodu #presunNa: pro přesun hráče na určené políčko. Uvedeme již rovnou všechny potřebné metody:

Browser -> Hrac -> hrani:

presunNa: aPolicko
  self opustAktualniPolicko.
  policko := aPolicko umistiHrace: self.

opustAktualniPolicko
  policko notNil
Výsledkem zaslání zprávy #notNil je pravdivostní hodnota (Boolean) true či false indikující, že v instanční proměnné není prázdná hodnota (nil).

    ifTrue: [ policko odstranHrace: self ]
V případě, že výsledkem testu je true, je vykonána část v hranatých závorkách, tedy políčko je požádáno o odstranění hráče. Jedná ve skutečnosti zase pouze o zaslání zprávy, a to přímo objektu true či false. Objekt true zareguje provedením kódu v hranatých závorkách -- tzv. blok (block) --, zatímco objekt false tento kód ignoruje.

Tady by se mohlo zdát, že bychom odstranění hráče mohli provést přímo, tím, že bychom "sáhli" do políčka a hráče z něj odstranili. To by však bylo proti principům čistého objektového programování: zasahovali bychom do "soukromí" jiného objektu, což se nemá. Kdo toto pravidlo porušuje, má sice na začátku snažší život (nemusí programovat další metodu), ovšem později spláče nad výdělkem: takto programovaný systém je velmi špatně udržovatelný a rozšiřitelný.
Browser -> Policko -> hrani:

odstranHrace: aHrac
  hraci remove: aHrac

umistiHrace: aHrac
  hraci add: aHrac
VIDEO 04

5. Experimenty s políčky a hráči

experimenty

Jednou z hlavních činností ve Smalltalku je "hraní si", a to při každém programování! Jakmile tedy máme hotovo něco, s čím si lze hrát, ihned se chopíme příležitosti a hrajeme si. My už nyní máme naprogramovaná políčka a hráče, tak můžeme začít experimentovat.

5.1 Zobrazení hrací desky

Workspace:

Hra ukazkovaHra -->Inspect
Vyvoláme Inspector na novou instanci z kontentového menu pravého tlačítka myši. V Inspectoru si můžeme prohlédnout celou strukturu ukázkové hry.


Čtěte dále, ale nezavírejte Inspector!

Takže máme vytvořenou instanci hry a umístili jsme hráče. Bylo by ale hezké si hrací plochu zobrazit. Mohli bychom nyní začít programovat webovou či desktopovou aplikaci, avšak zabývat se tvorbou takového složitého zobrazování nyní, když ještě nemáme dokončeno to základní -- logiku hry -- by nás odvádělo a rozptylovalo. Toto platí zcela obecně a ve Smalltalku vždy nejdříve programujeme struktury a základní logiku aplikace, vyzkoušíme je, ověříme si s případným zákazníkem, že to, co jsme udělali je skutečně tak, jak si to představoval a teprve když "střeva" šlapou tak, jak mají, je ten správný čas začít řešit "kabát". Proč je to tak správně? Důvodů je několik:

  • Můžeme se vždy soustředit na jednu věc, čímž jsme produktivnější.
  • Pokud se rozhodneme něco předělat (či na tom trvá zákazník), nezahodíme tolik práce.
  • Pokud programujeme velkou aplikaci, můžeme si v týmu lépe rozdělit role a specializovat se: následný vývoj webové či desktopové aplikace je možné předat jinému členovi týmu a pracovat na další aplikaci.

Nejjednodušší a nejrychlejší způsob zobrazování pro nás bude i nadále Inspector. Jak technicky probíhá zobrazování objektů v Inspectoru? Opět čistě objektově! Inspector objektu pošle zprávu #printOn:, na kterou objekt zareaguje vrácením své textové (String) reprezentace. Není tedy nic jednoduššího, než předefinovat příslušnou metodu, aby místo standardního "a ObjektXY" vracela něco výmluvnějšího:

Browser -> Policko -> tisk:

printOn: aStream
Tisk probíhá do tzv. "streamu", který možná znáte z jiných jazyků. V tomto případě si jej můžete představit jako "psací stroj", kam "naťukáme" pomocí zpráv #nextPutAll: řetězce textově reprezentující objekt.

  aStream nextPutAll: '[', pozice asString, ']'
Políčko bude reprezentováno pozicí v hranatých závorkách, tedy deska poté bude vypadat nějak takto: [1][2][3][4] atd. Do našeho "psacího stroje" tedy pošleme nejdříve hranatou závorku, za ní připojíme pomocí čárky stringovou reprezentaci pozice a uzavřeme hranatou závorkou.

Pozn.: Čárka je technicky zase pouhá zpráva, které rozumí objekt řetězec a zareaguje tím, že vrátí nový řetězec, který vznikne tak, že spojí svůj obsah s řetězcem, který je parametrem zprávy čárka. Aby se takovéto výrazy a také třeba aritmetické operace jako +, -, *, / daly psát takto elegantně a nemuseli jsme dávat dvojtečku jak jsme byli dosud zvyklí, jsou ve Smalltalku zavedeny tzv. binární zprávy (binary messages), které automaticky očekávají jeden parametr za názvem právy. Zprávy, které jsme dosud znali (tj. ty s dvojtečkou) se nazývají slovní zprávy (keyword messages). No a do třetice máme unární zprávy (unary messages), které žádný parametr nemají (což je zde např. self obsah).

Nyní můžeme metodu využít v zobrazení hrací desky:

Browser -> Hra -> tisk:

printOn: aStream
  policka do: [:kazde | kazde printOn: aStream]
Zde zasíláme kolekci policka zprávu #do:, která je tzv. iterátor, který "projede" kolekci a vytiskne všechna políčka (pošle jim zprávu #printOn:). Děje se to tak, že jako argument uvedeme blok. Zde se jedná o variantu bloku, který přijímá jeden vstupní parametr označený kazde. Iterátor funguje tak, že postupně bere prvky kolekce a volá blok, kde jako vstupní parametr vždy použije onen prvek.

Efekt naprogramování nových tiskových metod se hned projeví v běžícím Inspectoru!

VIDEO 05

5.2 Zobrazení hráčů na políčkách

Výpis má jeden zásadní nedostatek: není na něm přímo vidět hráče. Upravíme tedy metodu #printOn:, aby zobrazovala nejen pozici, ale i svůj obsah:

Browser -> Policko -> tisk:

printOn: aStream
  aStream nextPutAll: '[' , pozice asString , self obsah , ']'

obsah
  ^ self jePrazdne
      ifTrue: [ '' ]
      ifFalse: [ ' ', self vypisHracu ]
Zprávu #ifTrue: jsme si již vysvětlili výše u metody opustAktualniPolicko. Zde je použita zpráva #ifTrue:#ifFalse:, která ošetřuje variantu, kdy je podmínka splněna i variantu, kdy je nesplněna. Technicky se jedná skutečně o jednu zprávu s dvěma parametry.
Browser -> Policko -> testovani:

jePrazdne
  ^ hraci size = 0
Browser -> Policko -> tisk:

vypisHracu
  ^ hraci inject: '' into: [ :seznam :kazdy | seznam , ' ' , kazdy asString ]
Toto je poměrně pokročilý "fígl" s kolekcí. Nebudeme vás trápit dalšími informacemi, výsledkem prostě je řetězec seznamu hráčů oddělených čárkami. Pokud by vás #inject:into: přeci jen zajímal, mrkněte do některé knihy o Smalltalku třeba zde.
Browser -> Hrac -> tisk:

printOn: aStream
  aStream nextPutAll: jmeno

Pokud se nyní podíváme na ukázkovou hru v Inspectoru, vidíme již na prvním políčku naše dva hráče.

VIDEO 06

5.3 Zobrazování typů políček

Na hrací desce ale zatím nejsme schopni rozlišit jednotlivé typy políček: políčko obyčejné, políčko se žebříkem a políčko s hadem. Zde využijeme jedné šikovné vlastnosti čistého objektového programování a tou je polymorfismus. Ten umožňuje, že různé objekty mohou reagovat na stejnou zprávu různě, můžeme tedy naprogramovat metodu printOn: pro PolickoHadPolickoZebrik, které budou mít speciální chování, tedy vypisovat i kam nás políčko "hodí":

Browser -> PolickoHad -> tisk:

printOn: aStream
  aStream nextPutAll: '<-', dozadu asString.
Vytiskneme symbol <- následovaný počtem políček, o které vracíme.

  super printOn: aStream.
Zavoláme metodu #printOn: předka, tj. třídy Policko. Tím se nám vytiskne obsah políčka. Volání metody předka zajistíme zasláním zprávy objektu super místo self.

Efekt opět zkontrolujeme v otevřeném Inspectoru a pokračujeme:

Browser -> PolickoZebrik -> tisk:

printOn: aStream
  super printOn: aStream.
Tady nejdříve zavoláme metodu předka, tedy výpis obsahu políčka....

  aStream nextPutAll: dopredu asString , '+>'
...a až poté vypíšeme počet políček, o kolik posuneme a symbol +>.
VIDEO 07

6. Hráči chodí po desce

hráči chodí po desce

Strukturu hry máme vytvořenu, jdeme ji rozhýbat: naimplementujeme pohyb hráče, zatím však pro jednoduchost bez uvažování efektu hadů a žebříků.

6.1 Zatím bez dramat...

Browser -> Hrac -> hrani:

postupOHod: aKostka
  | hod destinace |
  hod := aKostka hod.
  destinace := policko dopreduO: hod.
  self presunNa: destinace.
  ^ jmeno , ' haze ', hod asString, ' a dostava se na ', policko asString
Výsledem volání metody bude textový řetězec s informací o provedeném hodu.
Browser -> Policko -> hrani:

dopreduO: pocet
Pro přesun mezi políčky využijeme programátorskou techniku známou jako rekurze (recursion), tedy volání sebe sama:

  pocet = 0
    ifTrue: [ ^ self ]
V případě, že zadaný počet políček, o kolik se posunout je nulový, tak se vlastně neposunujeme a tím pádem vracíme sebe.

    ifFalse: [ ^ self dalsiPolicko dopreduO: pocet - 1 ]
Jinak jdeme o alespoň jedno políčko dopředu, což v rekurzivním uvažování znamená posunout se na další políčko a poté jít dopředu o pocet-1. Tím, že zavoláme "sebe sama", objevíme se následně zase na začátku metody, ale budeme o políčko dál a bude snížený pocet. Znovu provedeme test na velikost pocet. Pokud není nulový, tak opět jdeme na další políčko a rekurzivně voláme. Toto se děje tak dlouho, dokud nebudeme na políčku, kde pocet=0 a tam skončíme (vrátíme self).


dalsiPolicko
  self assert: self jePosledni not.
Tento řádek prakticky "nepotřebujeme". Jedná se jen o bezpečnostní test, že nejsme na posledním políčku. #assert: je metoda, které rozumí každý objekt (tedy i naše Policko) a říká v překladu "předpokládej". Za assert tedy napíšeme, co má být splněno, aby metoda mohla proběhnout vpořádku a v případě, že podmínka splněna není, jsme na to poté při běhu upozorněni.

  ^ hra at: pozice + 1
Browser -> Policko -> testovani:

jePosledni
  ^ self == hra posledniPolicko
Zde jednoduše testujeme, jestli "já jsem poslední políčko hry". Jediné, co by vás tu mohlo zaskočit je použití dvojitého '==' místo jednoduchého. Zde se totiž ptáme na skutečnou identičnost objektu, nikoliv ekvivalenci. V těchto pojmech je drobný rozdíl: pokud budeme mít dva úplně stejné objekty, tj. dvě instance naplněné úplně stejnými daty, budou ekvivalentí (operace '=' bude vracet true), ale nebudou identické (oprace '==' bude vracet false).
Browser -> Hra -> testování:

posledniPolicko
  ^ policka last

at: aCislo
  ^ policka at: aCislo
Zde se jedná o techniku delegování (delegation), kdy zprávu pouze přeposíláme k vyřízení jinému objektu. Důvodem této na první pohled "neefektivní" konstrukce je opět snaha o budování do budoucna flexibilního a rozšiřitelného systému. Konkrétně zde ctíme pravidlo "do not talk to strangers" (nemluv na cizince), které je též nazýváno Law of Demeter (zákon Demeter). Zjednodušeně řečeno pravidlo říká, že objekt by neměl přímo "hovořit" k (tedy zasílat zprávy) vnitřku jiného objektu.

Implementovanou metodu #postupOHod: jdeme rovnou zkusit v Inspectoru!

Inspector:

| h k |
h := hraci at: 1.
k := Kostka new.
h postupOHod: k.

--> celé označíme a dáme Print it

Vidíme zprávu o hodu a výpis hry se ihned příslušným způsobem změnil. Nyní můžeme opakovat hod až ... ejhle ... objevuje se okénko se zprávou AssertionFailure: Assertion failed. Jedná se o tzv. výjimku (exception). Výjimka vždy signalizuje, že došlo k výjimečnému stavu. Může se jednat o chybu, v našem případě však byla výjimka vyvolána uměle, a to zprávou #assert: v metodě dalsiPolicko třídy Policko. Letmým obhlédnutím situace zjišťujeme, že skutečně je důvodem to, že jsme došli na poslední políčko a volali na něm metodu dalsiPolicko.

VIDEO 08

I když výjimka značí obvykle chybový stav, ve Smalltalku se však výjimek neděsíme, protože nám pomáhají vychytat nedostatky v programu. Díky tomu, že programujeme s Live Objects za běhu, výjimka nás často ani příliš nezdrží: doprogramujeme chybějící kód a pokračujeme dále.

Doprogramujeme tedy chování, aby hráč na posledním políčku zůstal a již se neposouval dále:

Browser -> Policko -> hrani:

dopreduO: pocet
  pocet = 0
    ifTrue: [ ^ self ]
    ifFalse: [
      self jePosledni
        ifTrue: [ ^ self ]
        ifFalse: [ ^ self dalsiPolicko dopreduO: pocet - 1 ] ]

V Inspectoru si můžeme zkusit (viz opět zelený rámeček výše), že nyní se hráč zastaví na poslední pozici a k výjimce nedojde...

VIDEO 09

6.2 A jde do tuhého...

Nyní doprogramujeme chování políček s hady a žebříky. Pokud na takovéto políčko hráč vstoupí, políčko ho "hodí" dozadu resp. dopředu. Každé políčko tedy "ví", na jaké destinaci má hráč skončit:

  • Policko: nikam nehází, hráč má tedy skončit na něm (self).
  • PolickoHad: destinace odpovídá pohybu o počet políček v instanční proměnné "dozadu".
  • PolickoZebrik: destinace odpovídá pohybu o počet políček v instanční proměnné "dopredu".

Zde opět využijeme výhod polymorfismu, které nám umožní, aby se destinace spočítala automaticky podle typu políčka, se kterým se zrovna bude pracovat.

Browser -> Policko -> hrani:

destinace
   ^ self
Browser -> PolickoZebrik -> hrani:

destinace
  ^ self dopreduO: dopredu

Nyní můžeme upravit metodu #dopreduO: tak, aby zafungovali hadi a žebříky a políčko tedy posunovalo na cílovou destinaci:

Browser -> Policko -> hrani:

dopreduO: pocet
  pocet = 0
    ifTrue: [ ^ self destinace ]
    ifFalse: [
      self jePosledni
        ifTrue: [ ^ self ]
        ifFalse: [ ^ self dalsiPolicko dopreduO: pocet - 1 ] ]

Analogicky vyřešíme posun zpět na políčku hada:

Browser -> PolickoHad -> hrani:

destinace
  ^ self zpatkyO: dozadu
Browser -> Policko -> hrani:

zpatkyO: pocet   pocet = 0
    ifTrue: [ ^ self destinace ]
    ifFalse: [
      self jePrvni
        ifTrue: [ ^ self ]
        ifFalse: [ ^ self predchoziPolicko zpatkyO: pocet - 1 ] ]
Browser -> Policko -> testovani:

jePrvni
  ^ pozice = 1
Browser -> Policko -> hrani:

predchoziPolicko
  self assert: self jePrvni not.
  ^ hra at: pozice - 1
VIDEO 10

6.3 Hotovo, otestujeme...

Celý pohyb po desce máme hotový, zbývá nám ho pořádně vyzkoušet. Abychom mohli ověřit, že všechny typy políček fungují tak, jak mají, nebudeme v #postupOHod: házet, ale budeme postupovat pouze o jedno políčko:

Browser -> Hrac -> hrani

postupOHod: aKostka
  | hod destinace |
  "hod := aKostka hod"
Zakomentujeme hod kostkou pomocí uvozovek ...

  hod := 1.
... a nahradíme jej jedničkou.

  destinace := policko dopreduO: hod.
  self presunNa: destinace.
  ^ jmeno , ' haze ', hod asString, ' a dostava se na ', policko asString

A jdeme to vyzkoušet. Začneme s novou hrou načisto:

Workspace:

Hra ukazkovaHra -->Inspect
Inspector:

| h k |
h := hraci at: 1.
h postupOHod: nil.

--> celé označíme a opakovaně provádíme pomocí Do it (Ctrl-D).

Funguje? Super! Až se dostatečně pokocháte, můžete vrátit zpět metodu #postupOHod: do původní podoby s hodem kostkou.

VIDEO 11

7. Hrajeme!

hrajeme

Nyní nám už zbývá pouze celou hru rozhýbat tak, abychom ji nemuseli ovládat ručně. Hra by měla sestávat z jednotlivých tahů (#zahrajTah), kdy se střídají hráči (#aktualizujNaTahu). Hra končí v okamžiku, kdy hráč po svém tahu končí na posledním políčku, což budeme testovat metodou #overVysledek.

Browser -> Hra -> hrani

zahrajTah
  | vysledek |
  dohrano
    ifTrue: [ ^ 'Hra je dohrana' ]
    ifFalse: [
      vysledek := (self aktualniHrac postupOHod: kostka) , self overVysledek.
Výsledkem bude hláška (string), která je generována jako výsledek metody #postupOHod: a za ní případně připojíme zprávu o vítězství, kterou bude vracet metoda #overVysledek.

      self aktualizujNaTahu.
      ^ vysledek ]

aktualniHrac
  ^ hraci at: naTahu

overVysledek
  self aktualniHrac policko == self posledniPolicko
    ifTrue: [
      dohrano := true.
      ^ ' -- ' , self aktualniHrac asString , ' vyhrava!' ]
    ifFalse: [ ^ '' ]

aktualizujNaTahu
  naTahu := 1 + (naTahu \\ hraci size)
Bude hrát další hráč. Operace \\ (modulo, zbytek po dělení) nám zajistí, že se nám z posledního hráče "přetočí" počítadlo zase na prvního (číslo 1).
Browser -> Hrac -> testovani

policko
  ^ policko

Nyní již můžeme pohodlně hrát celou hru:

Workspace:

Hra ukazkovaHra -->Inspect
Inspector:

self zahrajTah --> opakovaně Print it (Ctrl P)
VIDEO 12

Hra funguje, máme hotovo! Pokud bychom si ale chtěli hru zahrát opakovaně bez nutnosti ji znovu vytvářet (instanciovat), hodí se nám metoda #inicializujHru. Ta bude podobná jako metoda #initialize, ale zachováme políčka a hráče:

Browser -> Hra -> inicializace

inicializujHru
  kostka := Kostka new.
  naTahu := 1.
  dohrano := false.
  hraci do: [ :kazdy | kazdy presunNa: self prvniPolicko ]
Inspector:

self inicializujHru --> Do it
VIDEO 13

HOTOVO!

Odkazy

Software

  • Pharo -- open-source Smalltalk vývojové prostředí
  • Seaside -- fenomenální čistě objektový webový framework

Literatura

Zajímavé články