Piškvorky naslepo

Školení, která pořádám

Když Jirka Knesl zveřejní nějaký svůj kód, tak mám často nutkavou potřebu napsat vlastní řešení. Teď se mi to stalo s Piškvorkami naslepo. Tentokrát jsem nebyl sám, stejnou potřebu měl i Aleš Roubíček.

Přiznám se, že mi kód v Clojure přijde dost nečitelný v původní i refaktorované verzi. V PHP jsem napsal přímočaré řešení, které prostě jen řeší daný problém nejjednodušším způsobem:

<?php
echo "Ahoj v piškvorkách naslepo.
Povolené příkazy jsou:
new - nová hra
quit - konec
[a-i][0-9] - tah na pole, kde řada je pozice a, b, c, d, e, f, g, h, i. Sloupec je 1 až 9.
Formát zápisu je např. e5.
";

function isWinningInDirection(array $field, $x, $y, $a, $b) {
    // průzkum jedním směrem
    for ($i = 1; $i < 5; $i++) {
        $yb = $y - $i * $b;
        $xa = $x - $i * $a;
        if (!isset($field[$yb][$xa]) || $field[$yb][$xa] != $field[$y][$x]) {
            break;
        }
    }
    // průzkum druhým směrem
    for ($j = 1; $j < 5; $j++) {
        $yb = $y + $j * $b;
        $xa = $x + $j * $a;
        if (!isset($field[$yb][$xa]) || $field[$yb][$xa] != $field[$y][$x]) {
            return ($i + $j - 1 >= 5);
        }
    }
    return true;
}

function isWinning(array $field, $x, $y) {
    return isWinningInDirection($field, $x, $y, 1, 0)
        || isWinningInDirection($field, $x, $y, 0, 1)
        || isWinningInDirection($field, $x, $y, 1, 1)
        || isWinningInDirection($field, $x, $y, 1, -1)
    ;
}

function isFull(array $field) {
    foreach ($field as $row) {
        foreach ($row as $val) {
            if ($val == '_') {
                return false;
            }
        }
    }
    return true;
}

$playing = 'o';
while (true) {
    $field = array_fill(1, 9, array_fill(1, 9, '_'));
    while (true) {
        $playing = ($playing == 'x' ? 'o' : 'x');

        // získání vstupu od uživatele
        while (true) {
            echo "Hráč $playing: ";
            $input = rtrim(fgets(STDIN));
            if ($input == "new") {
                break 2;
            } elseif ($input == "quit") {
                echo "Naviděnou.\n";
                exit;
            }
            $y = ord(substr($input, 0, 1)) - ord('a') + 1;
            $x = substr($input, 1, 1);
            if (strlen($input) != 2 || !isset($field[$y][$x])) {
                echo "Tah ve špatném formátu.\n";
            } elseif ($field[$y][$x] != '_') {
                echo "Pole je zabráno, hraj znovu.\n";
            } else {
                break;
            }
        }
        $field[$y][$x] = $playing;

        if (isWinning($field, $x, $y)) {
            echo "VÝHRA! Gratulace hráči $playing.\n";
            break;
        } elseif (isFull($field)) {
            echo "Remíza, hrací pole zaplněno.\n";
            break;
        }
    }

    // zobrazení hracího pole
    foreach ($field as $row) {
        foreach ($row as $val) {
            echo "$val ";
        }
        echo "\n";
    }
    echo "Nová hra.\n";
}
?>

Na mé verzi kódu mi nejvíc vadí tři do sebe zanořené smyčky while (true). Víc by se mi líbilo, když by to byly do-while smyčky, ze kterých by bylo jasně patrné, čím končí. To by ale zase kód zkomplikovalo jiným způsobem. Získání vstupu od uživatele vyloženě volá o přesunutí do funkce – k tomu bych přistoupil, pokud by kód byl ve třídě, v současné verzi by předávání stavu bylo dost komplikované.

V hlavním kódu, který komunikuje s uživatelem, je i část herní logiky – konkrétně inicializace pole, změna hráče, zápis tahu a volání testů na remízu a výhru. Nevidím to ale jako velký problém – pokud bych hru chtěl udělat třeba pro prohlížeč, tak bych určitě využil funkce isWinning a isFull, ale zbytek bych napsal znovu. Přiznávám, že abstrahovat hru tak, aby se dala napojit jak na konzoli, tak třeba na web, by dalo dost práce.

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

Diskuse

Michal Pomykacz:

Ahoj, nebylo by lepší, kdyby funkce isFull() kontrolovala počet odehraných tahů proti velikosti hracího pole? Je to sice jen 9x9 deska, ale co kdybych si chtěl zahrát na větší :-)

ikona Jakub Vrána OpenID:

V téhle verzi ano.

Michal Novák:

Dle mého je nejhorší částí kódu rozhodně použití příkazu `break 2;`. Proč? Funkčnost kódu je závislá na struktuře řídících struktur pro smyčky. Takový kód je extrémně zranitelný při jakémkoliv refaktoru. Představte si nějaký blok kódu přenést do funkce nebo vložení nové smyčky či zrušení nadbytečné smyčky.

Použití break s argumentem je možná tak obhajitelné u jednorázových bash-like skriptů, které slouží jako nástroj.

ikona Jakub Vrána OpenID:

Souhlasím, souvisí to s mou výhradou na zanořenost while cyklů, i když to v textu není jasně uvedeno. Po zveřejnění článku jsem udělal ještě jednu verzi, která to řeší: https://gist.github.com/vrana/a9f2bdcea5d3d03c04c9

Jiří Urban:

Je tam drobná chybka. Když bude poslední krok zároveň výherním, kód to vyhodnotí tak, že je pole plné.

ikona Jakub Vrána OpenID:

Díky za objevení, to je školácká chyba. Opravil jsem to.

ikona Vojtěch Kohout:

Také jsem si neodpustil implementaci v PHP, pro změnu objektově orientovanou. Více informací u Jiřího. :)

https://github.com/Tharos/Tic-tac-toe

ikona Jakub Vrána OpenID:

Je to hezké, i když to je obecnější než původní hra (to nepovažuji nutně za výhodu). Líbí se mi, že když bych to místo z příkazové řádky chtěl ovládat z webu, tak stačí vyměnit třídu Game, je to tak?

ikona Vojtěch Kohout:

Máš pravdu, že to je obecnější a poznámka, že to není nutně výhoda, je trefná. Na implementaci se podepsal fakt, že jsem to psal jako takové cvičení pro zábavu a to člověku snadno „ujede ruka“. :) V praxi bych asi takhle zobecňoval pouze v případě, že bych tušil, že se daným směrem hra bude v budoucnu rozšiřovat.

Co se ovládání z webu týče, je to skutečně tak, že by stačilo nahradit třídu Game. Nicméně ještě by bylo zapotřebí vyřešit persistenci stavu hry mezi requesty. Zde bych určitě udržoval v session s uživateli nějaké ID konkrétní hry a k němu měl uložený stav hry třeba v SQLite. Je to hezký námět k rozšíření, asi to pro zajímavost naimplementuji ještě v boční větvi.

Mělo by to jít vyřešit jako čistá nadstavba, totiž bez zásahů do „herních“ tříd Board, Player, Players a Settings.

ikona Jakub Vrána OpenID:

Pro webovou verzi bych doporučoval Ratchet – http://socketo.me/. Já jsem si pomocí toho nedávno udělal síťové Prší v PHP+JS. Fakt, že se stav drží v paměti a není ho potřeba nikam ukládat a odnikud načítat, byl nesmírně osvěžující. Na tenhle druh aplikací je bezstavovost PHP dost nepříjemná.

ikona Vojtěch Kohout:

Tak jsem dotáhl tu webovou verzi: https://github.com/Tharos/Tic-tac-toe. Začal jsem ji psát ještě před tím, než jsi mi dal tip na Ratchet, a tak ten si dávám do pořadí. :)

Mám k aktuální implementaci dost postřehů:

– Nápad s SQLite byl úlet, to by mělo význam až tehdy, kdy by spolu hráli hráči po síti. Bohatě jsem si vystačil se session.

– Nakonec jsem musel sáhnout do třídy Players, ale jenom proto, protože jsem informaci o právě hrajícím hráči uchovával jako interní ukazatel v poli a ten bohužel nepřežije serializaci. Je to taková zajímavost, nikdy jsem se s tím nesetkal. :) Musel jsem to přepsat na méně mazanou implementaci.

– Na frontend jsem použil Nette, protože díky komponentového systému, šablonám, DI atp. mi ušetřilo hodně práce.

– Na tom řešení je krásně vidět, o čem píšeš. Ukládání toho stavu do session a jeho management je velmi neohrabaný a subjektivně nepřehledný. Komplikuje to fakt, že do session nestačí ukládat jenom stav pole plus to, kdo je na tahu, ale je zapotřebí ukládat i informaci, zda při posledním tahu došlo k nějaké specialitě (remíza, výhra jednoho z hráčů) a dále i informaci, zda je možné hrát dál.

– Celé by to šlo velmi snadno zAJAXovatět (například pomocí nette.ajax.js a ideálně i s pomocí history.ajax.js), ale z časových důvodů jsem se tímhle nezabýval. Stejně tak jako styly…

– OOP nezklamalo a nyní jsou obě varianty hry (webová i konzolová) plně funkční.

Ještě by mě hodně zajímalo, jak by vypadala implementace v nějakém moderním JS stacku (třeba v Este.js?).

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 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.