PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

Hry pro děti

Moje děti si občas chtějí zahrát nějakou hru. Tak už jsem jim několik her naprogramoval, občas i s jejich pomocí. Nešlo o to vytvořit něco dokonalého, ale rychle spíchnout něco, co bychom si mohli hned zahrát a snadno tomu přidávat další vlastnosti. Jednou jsme třeba někam letěli, tak jsem bez dostupnosti Internetu naprogramoval hru, kterou jsme se po zbytek letu bavili. Docela mě překvapilo, že celkem hratelná hra se vejde třeba i do 100 řádek JavaScriptu. Naštěstí jsem nemusel řešit, aby hry fungovaly ve všech prohlížečích, ale v dnešní době i tak nejspíš budou.


ukázka ze hryPrší

Klasická hra pro dva a více hráčů. Každý hráč hraje na svém zařízení, aby si vzájemně nekoukali do karet. Hry spolu komunikují pomocí WebSocket, což zajišťuje okamžitou viditelnost událostí na všech zařízeních bez nutnosti neustálého dotazování se serveru. Tím pádem je hra i trochu složitější na zprovoznění – nejprve je potřeba spustit server, pak ve zdrojáku nastavit, na jaké běží adrese a pak teprve otevřít hry v prohlížeči. Pro WebSockets jsem použil knihovnu Ratchet.


ukázka ze hryČlověče, nezlob se

Hra pro čtyři hráče. Pamatuji si, že se část stavu hry načítá přímo z DOMu, což je velmi krkolomné. Když bych hru programoval znovu, tak si veškerý stav uložím ve vlastních strukturách a do DOMu ho jenom promítám, nejspíš pomocí Reactu.


ukázka ze hryHad

Jednoduchá hra pro tři hráče. Po obrazovce lezou hadi, kteří žerou jídlo. Každý had může prolézt sám sebou, ale nesmí narazit do jiného hada, jinak chcípne a promění se v jídlo. Had může zrychlit, což ho zkracuje.

VlevoVpravoZrychlení
Hráč 1
Hráč 2ZXA
Hráč 3NMB

Hadi jsou tvořeni jednotlivými body, což může vést k trhanosti v pozdější fázi hry, kdy jsou hadi dlouzí. Při programování jsem musel oprášit velice pokročilou matematiku (goniometrické funkce):

snake.x = snake.x + Math.cos(snake.angle / 180 * Math.PI) * snake.speed;
snake.y = snake.y + Math.sin(snake.angle / 180 * Math.PI) * snake.speed;

ukázka ze hryMotokáry

Hra pro dva kráče. Po trati jezdí motokára. Když narazí do svodidel, tak zastaví. Vzájemně sebou motokáry můžou projíždět, aby se hráči neblokovali. Jde o to zajet nejrychlejší kolo.

Na čtverečkovaný papír jsem nakreslil trať, kterou jsem pak ručně přeťukal do kódu. Funkce arcTo, která se k tomu především používá, má dost nepříjemné API – přijímá bod, ke kterému má vyrazit rovná čára a druhý bod, ke kterému má čára zatočit. Obrázek v dokumentaci to hezky znázorňuje – ani jeden z bodů přijímaných funkcí na výsledné čáře není. Mnohem intuitivnější by bylo API, které by přijímalo cílový bod a poloměr křivky. Trať jsem kreslil pomocí API proto, že jsem si myslel, že křivky následně využiji pro detekci kolizí. Tu jsem ale nakonec vyřešil pomocí barvy bodů, takže jsem trať mohl hře předat i formou obrázku.

VlevoVpravoZrychleníBrzda/couvání
Hráč 1
Hráč 2ZCSX

Hra kontroluje, jestli motokára projela checkpointem v polovině trati, ale jinak lze trať projet oběma směry. Rychlost motokáry je pečlivě nastavena tak, aby se celá trať dala projet na plný plyn. Detekce kolizí: ctx.getImageData(x, y, 1, 1).data[0] != 255.

Jakub Vrána, Výuka, 24.10.2016, diskuse: 2 (nové: 2)

Ukládání dat do cookie

Na mobilním telefonu se běžně stává, že vypadne připojení k Internetu, a při ukládání dat na server bychom s tím měli počítat. Pokud data ukládáme běžným formulářem obnovujícím stránku, tak by se o to měl postarat prohlížeč – zobrazí informaci o nedostupnosti připojení a při jejím obnovení se data pošlou znovu. Pokud ale data odesíláme AJAXem, tak se o to musíme postarat sami.

Možné řešení spočívá v tom, že si data na klientu uložíme do cookie, která se při dalším úspěšném požadavku automaticky pošle na server. S knihovnou js-cookie můžeme cookie nastavit pomocí Cookies.set('save', data). Po uložení dat v cookie ji na straně serveru smažeme: setcookie("save", ""). Do cookie lze typicky uložit jen 4 KB dat, takže pro větší objemy dat se toto řešení bohužel nehodí.

Pokud je uživatel k aplikaci přihlášen a chceme, aby se změna dat projevila na všech jeho zařízeních (případně jde o změnu, která ovlivňuje i ostatní uživatele), tak je vhodné na server ihned odeslat požadavek pro uložení dat v cookie. Jen pokud se nepovede, tak data v cookie zůstanou a uloží se při dalším požadavku na server. Uživatele je v tom případě vhodné informovat o tom, že data ještě nejsou na straně serveru uložena.

Pokud se uložení dat má projevit jen v současném prohlížeči uživatele a nikde jinde (např. košík nepřihlášeného uživatele), tak extra požadavek na server ani posílat nemusíme a stačí počkat na přenos dat při dalším normálním požadavku.

A pokud stránku sestavujeme až na klientu, tak data na server ani nemusíme posílat a místo do cookie je můžeme uložit do localStorage.

Jakub Vrána, Řešení problému, 29.6.2016, diskuse: 4 (nové: 4)

Převod římských číslic

Nedávno jsem četl zajímavý článek Make Everything The Same o poznatcích z převodu na římské číslice. Článek je v kostce o tom, že odečítací číslice (např. IX = 9) vyžadují jiné zacházení než normální číslice (např. X = 10). Článek hovoří o podmínce, která má tendence se do kódu vkrádat a popisuje způsob, jak se jí zbavit. Ten spočívá v tom, že místo jedné konverze do finálního tvaru uděláme dvě konverze – první do aditivní formy (nepoužívá odečítací číslice) a druhou z aditivní formy do finální podoby.

Oceňuji přístup se vším zacházet stejně a nepoužívat zbytečně podmínky, ale popsané řešení mi přijde zbytečně krkolomné. Tady je moje řešení:

<?php
class RomanNumerals {
    private static $romanToNumber = array(
        'M' => 1000, 'CM' => 900,
        'D' => 500, 'CD' => 400,
        'C' => 100, 'XC' => 90,
        'L' => 50, 'XL' => 40,
        'X' => 10, 'IX' => 9,
        'V' => 5, 'IV' => 4,
        'I' => 1,
    );

    /** Převod na římské číslice
    * @param int
    * @return string
    * @copyright Jakub Vrána, https://php.vrana.cz/
    */
    static function toRoman($n) {
        $return = '';
        foreach (self::$romanToNumber as $roman => $number) {
            $times = floor($n / $number);
            $return .= str_repeat($roman, $times);
            $n -= $number * $times;
        }
        return $return;
    }
}
?>

Vytvoříme si jen jednu mapu, ve které budou všechny převody a projdeme ji. Žádné podmínky v kódu nejsou. Mapa by se dala vytvořit i programově, ale to by bylo poměrně krkolomné.

Převod z římských číslic

Mnohem zajímavější je převod z římských číslic. Pro ten můžeme použít stejnou převodní mapu a následující kód:

<?php
class RomanNumerals {
    /** Převod z římských číslic
    * @param string
    * @return int
    * @copyright Jakub Vrána, https://php.vrana.cz/
    */
    static function fromRoman($s) {
        $return = 0;
        $pos = 0;
        foreach (self::$romanToNumber as $roman => $number) {
            while ($pos < strlen($s) && substr_compare($s, $roman, $pos, strlen($roman)) == 0) {
                $return += $number;
                $pos += strlen($roman);
            }
        }
        if ($pos != strlen($s)) {
            throw new Exception("Invalid roman number: $s");
        }
        return $return;
    }
}
?>

Kód kontroluje, jestli jsou číslice seřazené od nejvyšší po nejnižší a jestli řetězec neobsahuje nepovolený znak (což jsou nutné podmínky při zápisu římských číslic). Nekontroluje počet opakování jednotlivých číslic (např. IIIII = 5 nebo IXIX = 18 projde) ani kompaktnost zápisu (např. VIV = 9 projde, i když se dá zapsat jako IX = 9). Podle článku na Wikipedii jsou ale takové zápisy možné. Pokud bychom chtěli zkontrolovat, jestli je číslo v kanonické formě, tak by asi nejjednodušší bylo na výsledku zavolat toRoman a podívat se, jestli vrátilo náš vstup.

Viz též můj starší článek na stejné téma, který jsem objevil až po dopsání tohoto. A protože u minulého článku bylo nejvíc dotazů na to, jak se převede nějaké konkrétní číslo, tak ještě formulář:

Arabské nebo římské číslo:

Jakub Vrána, Řešení problému, 17.6.2016, diskuse: 2 (nové: 2)

Protobuf a NaN

Většina produktů Google komunikuje téměř výhradně pomocí Protocol Buffers. V tomto formátu se komunikuje s úložištěm, mezi servery a posílají se v něm data i do klienta a z klienta. Na serveru se protocol buffery přenáší v binárním formátu, na klienta se posílá řetězec obsahující JavaScriptový literál pro pole. Ve srovnání třeba s JSONem jde o formát mnohem úspornější, protože názvy polí se vůbec nepřenáší. Srovnejte:

{"id":1,"name":"Jakub"} // JSON
[1,"Jakub"] // Protobuf

Při buildu se vygenerují metody jako getName a setName, pomocí kterých se s protocol bufferem pracuje. To má zároveň tu výhodu, že když uděláte v názvu metody překlep, tak build spadne. U JSONu tato ochrana chybí, protože např. obj.anme je platná hodnota při čtení i zápisu. Closure Compiler vygenerované metody potom obvykle zase inlinuje, taže i výsledný JavaScriptový kód je malý.

Na klientu se řetězec obsahující protocol buffer dá přeložit do JavaScriptového pole v zásadě dvěma způsoby: eval nebo JSON.parse. Oba způsoby jsou v moderních prohlížečích velmi rychlé, JSON.parse o něco rychlejší. eval má zase tu výhodu, že nepoužitá pole lze vynechat: eval('[1,,,"Jakub"]') versus JSON.parse('[1,null,null,"Jakub"]'). Tahle výhoda ale není příliš zásadní, protože mnohokrát opakovaný fragment ,null lze dobře komprimovat. Při mnoha prázdných polích se pak použije formát {"1":1,"2000":"Jakub"}.

Většina produktů Google v současnosti pro zpracování protocol bufferů používá eval. Důvod je ten, že JSON nepodporuje speciální číselné hodnoty, které podporuje protobuf i JavaScript. Jde o NaN, Infinity a -Infinity. Nekonečno se dá simulovat pomocí obřího čísla (1e404 se zaokrouhlí na Infinity), ale s NaN je problém. Přenášená data jsou tak rozmanitá, že tu a tam se NaN bohužel někde vyskytne.

Rozhodli jsme se proto NaN přenášet jako řetězec "NaN" a při deserializaci ho přeložit zase zpátky na NaN. Opačnou změnu je samozřejmě potřeba udělat i při serializaci, protože JSON.stringify převádí NaN na null. Díky tomu budeme moci z kódu odstranit eval a z CSP potom vyhodit unsafe-eval.

Jakub Vrána, Řešení problému, 19.2.2016, diskuse: 0 (nové: 0)

Adminer 4.2.4

Nová verze Admineru opravuje závažnou bezpečnostní chybu: vzdálené spuštění. Může k němu dojít za těchto okolností:

Ke vzdálenému spuštění mohlo dojít použitím SQLite příkazu ATTACH, který dovoluje vytvořit novou databázi. Pokud tato databáze má název *.php a následně je do ní vložen PHP kód, tak šlo tento kód následně spustit. SQLite sice umožňuje příkaz ATTACH omezit pomocí SQLITE_LIMIT_ATTACHED, ale PHP tento limit nedovoluje nastavit. Adminer proto příkaz ATTACH zakazuje parsováním SQL dotazu.

U správně zabezpečeného serveru se tato chyba neprojeví:

  1. Používejte jen verzi Admineru s ovladači, které potřebujete (např. samotné MySQL).
  2. Přístup k Admineru zabezpečte buď pomocí rozšíření (metoda login) nebo externě (např. v Apache souborem .htaccess).
  3. Uživateli, pod kterým běží web, seberte práva zápisu. Pokud je zápis výjimečně někam potřeba povolit, tak jen do adresáře nedostupného z webu. Pokud tento adresář musí být dostupný z webu, tak v něm zakažte spouštění PHP skriptů např. pomocí engine Off.
  4. Adminer pokud možno provozujte na samostatném serveru, z kterého je přístup do databáze, ale nikam jinam.

Za objevení a nahlášení chyby děkuji uživateli 庞申杰(千霄) ze společnosti Alibaba.

Jakub Vrána, Adminer, 6.2.2016, diskuse: 10 (nové: 10)

Starší články naleznete v archivu.

avatar © 2005-2016 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.