PyLadies Brno

Klondike Solitaire

Uděláme karetní hru, kterou možná znáš v některé z jejich počítačových verzí. Vypadá takhle:
Ale protože my ještě neumíme kreslit obrázky, ani pracovat s počítačovou myší, naše verze bude vypadat takto:
  U     V          W     X     Y     Z
[???] [   ]      [   ] [   ] [   ] [   ]

  A     B     C     D     E     F     G
[3♣ ] [???] [???] [???] [???] [???] [???]
      [5 ♥] [???] [???] [???] [???] [???]
            [6♣ ] [???] [???] [???] [???]
                  [5♠ ] [???] [???] [???]
                        [Q ♥] [???] [???]
                              [4♠ ] [???]
                                    [3 ♦]

Zadej tah: _

Pravidla

  • Hra se hraje s náhodně zamíchaným balíčkem 52 karet, kde je jedna karta pro každou kombinaci hodnota-barva za čtyř barev (♠, , , ♣) a 13 hodnot (2-10, J – kluk, Q – dáma, K – král, A – eso).
  • Obrázkové karty mají pro účely této hry hodnoty: A=1, J=11, Q=12, K=13.
  • 28 karet se rozdá do sedmi sloupců (A-G), přičemž první sloupec má jednu kartu, druhý dvě, třetí tři, atd. Vrchní karta každého sloupce se otočí lícem nahoru, ostatní jsou lícem dolů. Zbylé karty tvoří balíček (U). Kromě sloupců a balíčku je na stole vyhrazené místo pro otočený balíček (V) a cílové hromádky (W-Z).
  • V každém tahu může hráč udělat jednu z těchto akcí:
    • Přemístit kartu z konce některého sloupce (A-G) nebo z otočeného balíčku (V) na cílové políčko.
    • Přemístit vrchní kartu z otočeného balíčku na konec některého sloupce (A-G).
    • Přemístit n karet z konce jednoho sloupce na konec druhého sloupce. Karty k přemístění musí být otočeny lícem nahoru.
    • Otočit vrchní kartu z balíčku (U) lícem nahoru, a dát ji na vršek otočeného balíčku (V).
    • Otočit celý otočený balíček (V) a dát ho na spodek balíčku (U). Tohle se dělá jen pokud je balíček (U) prázdný.
  • Pravidla pro dávání karty na cílové políčko (W-Z):
    • Eso se dá dát pouze na prázdné cílové políčko.
    • Ostatní karty se dají dávat jen na cílové políčko, které má na vršku kartu stejné barvy a hodnoty o 1 nižší.
    Neboli, každé cílové políčko je na karty jedné barvy, kam se skládají od es po krále.
  • Pravidla pro dávání karet na konec sloupce:
    • Pokud je sloupec prázdný, dá se na něj položit pouze postupka začínající králem.
    • Pokud sloupec není prázdný, dá se na něj položit pouze karty, které tvoří postupku s původní kartou na konci sloupce.
  • Pravidlo pro braní karet ze sloupce:
    • Pokud po odebrání karet ze sloupce je karta na konci sloupce otočená rubem nahoru, otočí se lícem nahoru.
  • Postupka je v této hře sekvence karet, kde:
    • karta na konci má nejnižší hodnotu, a každá další karta má hodnotu o 1 vyšší než karta předchozí, a
    • pokud je karta červená ( nebo ), další karta (pokud nějaká je) musí být černá (♠ nebo ♣), a naopak.
  • Hra končí, když je balíček a všechny sloupce (A-G, U, V) prázdný. Neboli když jsou všechny karty v cíli (W-Z).

Ovládání

Ovládání takovéhle textové hry není úplně přímočaré, ale snad se bude dát pochopit:
Příkazy:
? - Vypíše tuto nápovědu.
U - Otočí kartu balíčku (z U do V).
    Nebo doplní balíček U, pokud je prázdný.
EC - Přemístí kartu z E na C.
     Za E dosaď odkud kartu vzít: A-G nebo V.
     Za C dosaď kam chceš kartu dát: A-G nebo W-Z.
E2G - Přemístí 2 karty z E na C
      Za E dosaď odkud kartu vzít: A-G nebo V.
      Za 2 dosaď počet karet.
      Za C dosaď kam chceš kartu dát: A-G nebo W-Z.
Ctrl+C - Ukončí hru

Zobrazování

Protože zatím neumíme psát barvičky, a symboly ♠♥♦♣ se můžou plést, budeme červené karty psát s mezerou mezi číslem a hodnotou ([5 ♥]) a černé s mezerou před číslem ([3♣ ]). Snad to pro rozlišení červené a černé bude stačit.
Aby se nám hodnota vešla do jednoho znaku, desítku budeme ukazovat římsky. Třeba [X♣ ] je piková desítka.
Na Windows možná budeš mít problémy s vypisováním znaků ♠, , , ♣ – některé windowsí terminály umí jen anglické a české znaky, a když se budeš snažit vypisovat exotické klikyháky, Python zahlásí výjimku UnicodeDecodeError. Kdyby se to stalo, nahraď ♠ za P, za S, za K, a ♣ za +.

Datové struktury

Nejdůležitější věc, kterou musíme při psaní této hry udělat, je rozmyslet si, jak si program bude „pamatovat“ stav hry.
Potřebujeme ten stav nějak poskládat z datových typů které známe – čísla, řetězce, seznamy, n-tice.
Tentokrát si tuhle část projdeme společně.

Karta

Jaké vlastnosti karty si potřebujeme pamatovat? Každá karta má hodnotu a barvu. Navíc může každá karta ve hře být otočená lícem nebo rubem nahoru. Potřebujeme si tedy ke každé kartě pamatovat tři informace. A na tři různorodé informace je nejlepší použít n-tici – v našem případě trojici „(hodnota, barva, otoceni)“. Ale co dávat do jednotlivých „políček“?

Hodnota

Hodnota karty může být 2-10 nebo J, Q, K, A. Ve hře ale budeme muset porovnávat hodnoty a kontrolovat postupky (kde po sobě následující karty musí mít hodnoty x a x+1), což se dělá mnohem lépe s čísly než s mixem čísel a řetězců.
Obecně bývá dobré „vevnitř“ v programu používat takovou reprezentaci informací, se kterou se nejlíp počítá, a teprve až když se ty hodnoty ukazují uživatelům (nebo od nich dostávají), tak se převedou na něco (nebo z něčeho) co dává smysl lidem.
Pamatujme si tedy hodnotu jako číslo od 1 (eso) po 13 (král).

Barva

U barev nebudeme kontrolovat postupky, takže není důvod tady používat čísla. Co se operací s barvami týče, stejně dobře poslouží řetězce, čísla nebo jakékoliv jiné hodnoty – jen musí být 4 různé.
Obecně když je jedno jestli použít čísla nebo řetězce (nebo jiný typ), je dobré použít krátké řetězce – když pak něco nepovede, je lepší si v chybové hlášce mít „piky“ než „barva 3“.
V našem programu tedy použijeme jako barvu vždy první dvě písmena názvu: 'Pi', 'Sr', 'Ka', 'Kr' pro, respektive, ♠, , a ♣.

Otočení

Otočení karty může mít dvě hodnoty – lícem nebo rubem navrch. Na dva možné stavy můžeme použít bool, tedy True nebo False.
Aby se líp pamatovalo, kterému stavu jsme přiřadily True a kterému False, nepojmenujeme příslušnou proměnnou otoceni, ale licem_nahoru.

Práce s kartou

Kartu budeme tedy reprezentovat jako trojici čísla, řetězce, a boolu. Rychle si zopakujme, jak se taková n-tice tvoří, a jak se zase „rozkládá“ do jednotlivých částí:
srdcova_kralovna = 12, 'Sr', True
schovane_eso = 1, 'Kr', False

hodnota, barva, licem_nahoru = schovane_eso
Vytvoř funkci popis_karty, která dostane jako argument kartu (trojici), a vrátí [???] pro kartu, která je rubem nahoru, nebo příslušný řetězec (např. [3♣ ] či [X ♥]) pokud je lícem nahoru.
Ve Windows s terminálem bez exotických znaků to bude [3+ ] a [X S].
Dále budeme potřebovat mechanismus na otáčení karet – funkci, která změní otočení ale zachová hodnotu a barvu. Protože n-tice se nedají měnit, musíme ve funkci otoc_kartu udělat novou trojici se stejnými hodnotami, kterou pak vrátíme:
def otoc_kartu(karta, nove_otoceni):
    hodnota, barva, licem_nahoru = karta
    return hodnota, barva, nove_otoceni
Soubor s funkcemi ulož jako klondike.py, a pomocí těchto testů si ověř, že všechno funguje jak má.
Jak soubor s testy tak klondike.py dej do stejného adresáře, a z toho samého adresáře pusť py.test. Nezapomeň mít aktivované virtuální prostředí, kam jsi si před pár týdny nainstalovala pytest.

Balíček

Jak si repreentovat balíček či sloupec karet? Je to nějaká sekvence karet, která může mít různou délku. Na takové věci je ideální použít seznam.
Seznamy mají spoustu metod které pracují s prvky na konci, jako například append a pop. Oproti tomu na začátek seznamu se přidávají prvky složitěji. Na posledním místě seznamu (balicek[-1]) tedy budeme mít karty, se kterými se bude víc pracovat – vršek balíčku nebo konec sloupce.
Sloupec C ze hry na začátku této kapitoly by mohl být reprezentován tímto seznamem:
sloupec_c = [
    (12, 'Sr', False),  # Srdcová dáma, rubem nahoru
    (7, 'Ka', False),   # Kárová sedma, rubem nahoru
    (6, 'Kr', True),    # Křížová šestka, lícem nahoru
]

Vypsání balíčku

Vytvoř funkci popis_balicku, která dostane jako argument balíček (seznam trojic), a vrátí popis vrchní karty, nebo [   ] pokud je balíček prázdný.
Pomocí těchto testů si ověř, že funkce funguje jak má.

Hra

Celý stav hry se skládá ze spousty seznamů karet:
  • Dva balíčky (U a V)
  • Čtyři cílové hromádky (W, X, Y, Z)
  • Sedm sloupečků (A, B, C, D, E, F, G)
Aby v tom byl pořádek (a taky abychom si procvičily práci s vnořenými n-ticemi a seznamy), budeme si hru pamatovat jako (dvojici balíčků, čtveřici hromádek, a sedmici sloupečků).
Záčáteční stav hry tedy bude vypadat zhruba takhle:
balicky = [...], []        # dvojice: seznam karet, a prázdný seznam
hromadky = [], [], [], []  # čtveřice prázdných seznamů
sloupecky = [...], [...], [...], [...], [...], [...], [...]  # sedmice seznamů karet

hra = balicky, hromadky, sloupecky  # trojice
Máme tedy, podtrženo sečteno, trojici n-tic seznamů trojic. To zní docela složitě. Když si ale budeme dávat pozor, snad se do toho příliš nezamotáme :)
Jeden způsob jak se nezamotat je používat proměnné s názvy, které vystihují co která proměnná obsahuje. Místo něčeho jako:
hra[0]              # balíčky (U a V)
hra[0][0]           # balíček U
hra[0][0][-1]       # vrchní karta v balíčku U
hra[0][0][-1][0]    # hodnota vrchní karty v balíčku U
napíšeme třeba tohle:
balicky, hromadky, sloupecky = hra
balicek_U, balicek_V = balicky
vrchni_karta = balicek_U[-1]
hodnota, barva, licem_nahoru = vrchni_karta
Ale dost přemýšlení o datových strukturách, začněme dělat hru.

Program

Takhle bude vypadá soubor hra.py, který budeme spouštět abychom si zahrály. Funguje velice podobně jako u 1D piškvorek: je tam smyčka, která vypíše stav, načte tah, provede tah, a tak stále dokola. Jediné co je navíc je funkce priprav_tah, která bude mimo jiné kontrolovat, jestli je tah v pořádku.
import klondike

hra = klondike.udelej_hru()
klondike.vypis_hru(hra)
while not klondike.hrac_vyhral(hra):
    tah = klondike.nacti_tah()
    try:
        info = klondike.priprav_tah(hra, tah)
    except ValueError as e:
        print(e)
    else:
        klondike.udelej_tah(hra, info)
        klondike.vypis_hru(hra)
Zbývá jen napsat jednotlivé funkce!

Inicializace

Jak udělat balíček karet, to už víme z minula:
import random

balicek = []
for hodnota in range(1, 14):
    for barva in 'Pi', 'Sr', 'Ka', 'Kr':
        balicek.append((hodnota, barva, False))
random.shuffle(balicek)
Napiš funkci udelej_hru, která nebere žádné argumenty, a vytvoří hru. Postup:
  • Vytvoř zamíchaný balíček 52 karet
  • Vytvoř (pomocí for/append) seznam sedmi sloupečků. Do každého dej při vytváření určitý počet karet. Kartu vždy lízni ze zamíchaného balíčku (pomocí metody pop), otoč (pomocí funkce otoc_kartu, a dej do sloupečku (pomocí metody append).
    • Do prvního sloupečku dej 0 karet lícem dolů, a 1 kartu lícem nahoru.
    • Do druhého sloupečku dej 1 kartu lícem dolů, a 1 lícem nahoru.
    • Do třetího dej 2 lícem dolů, a 1 nahoru.
    • Do čtvrtého 2 lícem dolů a 1 nahoru.
    • ... atd.
  • Seznam sedmi sloupečků převeď na sedmici pomocí funkce tuple.
  • Vytvoř dvojici balíčků, čtveřici prázdných hromádek, a sedm sloupečků, a ty vrať.
Kdyby ses do toho zamotala, pusť následující program, který všechny ty struktury vypíše trošičku srozumitelně. (Vzorový výstup je tady.)
import klondike
import pprint
pprint.pprint(klondike.udelej_hru())
Funkci si opět ověř pomocí testů.

Výpis hry

Výpis z pprint by pro hráče nebyl příliš příjemný (a hlavně by z něho viděli i zakryté karty), a tak napíšeme funkci vypis_hru, která bude tvořit „hezkou“ „grafiku“. Rozehraná hra bude vypadá nějak takhle:
Na rozdíl od ostatních funkcí, které jsme zatím dělaly, tahle bude přímo používat příkaz print, a nebude nic vracet.
Výstup bude vypadat nějak takhle (pro rozehranou hru):
  U     V           W     X     Y     Z
[???] [9♠ ]       [A♠ ] [2♣ ] [2 ♦] [   ]

  A     B     C     D     E     F     G
[8♠ ] [???] [???] [???] [???] [???] [???] 
[7 ♦] [X ♥] [???] [???] [???] [???] [???] 
      [9♣ ] [K ♥] [???] [4♣ ] [???] [???] 
      [8 ♥] [Q♠ ] [J ♦]       [???] [???] 
                  [X♠ ]       [???] [???] 
                              [5 ♥] [???] 
                              [4♠ ] [K♠ ] 
                              [3 ♦]       
Postup:
  • Z argumentu hra vypreparuj jednotlivé balíčky, hromádky, a sedmici sloupečků.
  • Napiš řádek U V W X Y Z, se správnými mezerami – tohle si zkopíruj z příkladu.
  • Pomocí funkce popis_balicku, kterou zavoláš šestkrát, vypiš druhý řádek.
  • Napiš prázdný řádek a pak řádek A B C D E F G.
  • Zjisti maximální délku sloupečku: Na začátku ji nastav na 0, pak projdi všechny sloupečky a když potkáš větší délku než jsi zatím viděla, aktualizuj ji.
  • Projdi řádky (od 0 do max. délky sloupečku-1). V každém řádku projdi všechny sloupečky, a vypiš kartu sloupecek[i]. (Když dostaneš IndexError, sloupeček už skončil – vypiš příslušný počet mezer.)
Funkci ověř pomocí testů. Pokud jsi na Windows a vypisuješ náhradní znaky za ♠♥♦♣, nastav na začátku testovacího souboru ASCII_ONLY = True.

Kontrola vítězství

Hráč vyhrál, pokud v balíčcích ani sloupečcích nezbývá žádná karta. A nebo pokud na vršku všech cílových hromádek jsou králové. Kterou variantu použiješ ve funkci hrac_vyhral, to je na tobě.
Odkaz na testy je tady.

Načtení tahu

Funkce nacti_tah se zeptá uživatele, co chce dělat. Tahle funkce příliš nepracuje s n-ticemi a seznamy, tak ji sem pro zrychlení napíšu. Přečti si ale její dokumentační řetězec, ať víš co dělá:
MOZNOSTI_Z = 'ABCDEFGV'
MOZNOSTI_NA = 'ABCDEFGWXYZ'
NAPOVEDA = """
Příkazy:
? - Vypíše tuto nápovědu.
U - Otočí kartu balíčku (z U do V).
    Nebo doplní balíček U, pokud je prázdný.
EC - Přemístí karty z E na C.
     Za E dosaď odkud karty vzít: A-G nebo V.
     Za C dosaď kam chceš karty dát: A-G nebo W-Z.
E2G - Přemístí 2 karty z E na C
      Za E dosaď odkud kartu vzít: A-G nebo V.
      Za 2 dosaď počet karet.
      Za C dosaď kam chceš kartu dát: A-G nebo W-Z.
Ctrl+C - Ukončí hru
"""

def nacti_tah():
    """Zeptá se uživatele, co dělat

    Stará se o výpis nápovědy.

    Může vrátit buď řetězec 'U' ("lízni z balíčku"), nebo trojici
    (z, pocet, na), kde:
        - `z` je číslo místa, ze kterého karty vezmou (A-G: 0-7; V: 8)
        - `pocet` je počet karet, které se přemisťují
        - `na` je číslo místa, kam se karty mají dát (A-G: 0-7, W-Z: 8-11)

    Zadá-li uživatel špatný vstup, zeptá se znova.
    """
    while True:
        retezec = input('Zadej tah: ')
        retezec = retezec.upper()
        if retezec.startswith('?'):
            print(NAPOVEDA)
        elif retezec == 'U':
            return 'U'
        elif len(retezec) < 2:
            print('Nerozumím tahu')
        elif retezec[0] in MOZNOSTI_Z and retezec[-1] in MOZNOSTI_NA:
            if len(retezec) == 2:
                pocet = 1
            else:
                try:
                    pocet = int(retezec[1:-1])
                except ValueError:
                    print('"{}" není číslo'.format(retezec[1:-1]))
                    continue
            tah = (MOZNOSTI_Z.index(retezec[0]), pocet,
                   MOZNOSTI_NA.index(retezec[-1]))
            return tah
        else:
            print('Nerozumím tahu')

Příprava tahu

Nyní napíšeš funkci priprav_tah, která zkontroluje, že zadaný tah je podle pravidel, a vrátí informace o tom, jaký tah přesně provést. Nebude ovšem ještě provádět žádnou akci.
Mohlo by se zdát, že funkce nacti_tah a priprav_tah dělají v podstatě tu stejnou práci – načítají tah od uživatele. Ptáš se, proč jsou oddělené? Je to hlavně kvůli testování – nacti_tah používá input(), takže se špatně testuje, a proto by měla být co nejmenší. A v priprav_tah bude zakódována většina pravidel hry, takže by měla být otestována co nejlépe.
Ze začátku se ale na pravidla vykašleme, a necháme hráče přemisťovat karty dle libosti – tak jako by hrál s opravdovými papírovými kartami. Bude zatím na samotných hráčích, aby hráli podle pravidel.
def priprav_tah(hra, tah):
    """Zkontroluje, že je tah podle pravidel

    Jako argument bere hru, a tah získaný z funkce `nacti_tah`.

    Vrací buď řetězec 'U' ("lízni z balíčku"), nebo trojici
    (zdrojovy_balicek, pocet, cilovy_balicek), kde `*_balicek` jsou přímo
    seznamy, ze kterých/na které se budou karty přemisťovat, a `pocet` je počet
    karet k přemístění.

    Není-li tah podle pravidel, vynkce vyvolá výjimku `ValueError` s nějakou
    rozumnou chybovou hláškou.
    """
    balicky, cile, sloupce = hra
    if tah == 'U':
        return 'U'
    else:
        z, pocet, na = tah
        if z == 7:
            zdrojovy_balicek = balicky[1]
        else:
            zdrojovy_balicek = sloupce[z]
        karty = zdrojovy_balicek[-pocet:]
        if na < 7:
            cilovy_balicek = sloupce[na]
        else:
            cilovy_balicek = cile[na - 7]
        return zdrojovy_balicek, pocet, cilovy_balicek

Provedení tahu

Následuje funkce, která provede tah podle informací, kteŕe dostane z priprav_tah.
Jak by tahle funkce měla fungovat:
  • Pokud dostane 'U', a v balíčku U něco je, lízne vrchní karu (pomocí metody pop), otočí ji (pomocí funkce otock_kartu), a dá ji na vršek balíčku V (pomocí metody append).
  • Pokud dostane 'U', a v balíčku U nic není, postupně všechny karty z V lízne, otočí, a dá na V.
  • Jinak trojici, kterou dostala, rozloží na zdrojový_balíček, počet, a cílový_balíček. Přidá počet karet ze zdrojového balíčku, přidá je na cílový balíček pomocí metody extend, a smaže je ze zdrojového balíčku.
    Potom, pokud je na vršku zdrojového balíčku karta otočená rubem nahoru: obrátí tuhle kartu (lízne, otočí, a přidá tam, odkud ji lízla).
Funkci zase otestuj; testy stáhni tady.
Povedlo se? Gratuluji! Máš funkčí hru! Zbývá k ní jen dopsat kontrolu pravidel.

Kontrola pravidel

Všechno kontrolování bude probíhat ve funkci priprav_tah. Zkus do ní dopsat následující hlášky:
  • 'Z balíčku V se nedá brát víc karet najednou!'
  • 'Na to není v {pismeno} dost karet!'
  • 'Nemůžeš přesouvat karty, které jsou rubem nahoru!'
  • 'Do prázdného sloupečku smí jen král!'
  • 'Do cíle se nedá dávat víc karet najednou!'
  • 'Do prázdného cíle smí jen eso!'
  • 'Cílová hromádka musí mít jednu barvu!'
  • 'Do cíle musíš skládat karty postupně od nejnižších!'
Testy jsou tuhle: test_priprav_tah.

Postupky

A nakonec to nejtěžší. Zkus, přijít na to, jak kontrolovat jestli je v seznamu karet postupka. Udělej na to funkci zkontroluj_postupku, která jako argument bere seznam karet, a buď nic nevrátí nebo vyvolá výjimku ValueError s jedním z textů:
  • 'Musíš dělat sestupné postupky!'
  • 'Musíš střídat barvy!'
I na tuhle funkci pusť testy.
Potom, co tu funkci napíšeš, ji na vhodném místě zavolej.