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ší,
v první verzi si zobrazování a zadávání tahů
trochu zjednodušíme.
Výsledek 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:
Tohle je mnohem větší a složitější projekt,
než jaké jsme dělaly doposud.
Pojďme si upřesnit, co bude náš program dělat.
Takováhle specifikace je důležitá část
tvorby programu, a většinou se nezískává snadno,
protože na začátku není nikomu – ani programátorovi,
ani možným uživatelům či zákazníkovi – jasné,
co je přesně potřeba udělat.
Náš projekt bude tedy zjednodušený v tom,
že budeme od začátku vědět co chceme udělat.
(Další rozšíření, třeba grafická verzi s obrázky
a taháním karet myší, už ale budou jen na tobě.)
Pravidla
Neznáš-li tuhle hru, zkus si ji zahrát (nějaká
verze by měla být k dispozici na všechny operační
systémy, nebo se dá najít na internetu).
Přesná pravidla hry jsou tady (s použitím písmenek
v „diagramu” výše).
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.
E2C - 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.
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 +
.
Skoro všechny hodnoty karet (2-9, J, Q, K, A) se
vejdou do jednoho znaku. Aby se vešla i desítka,
budeme ji ukazovat římským číslem, X
.
Třeba [X♣ ]
je piková desítka.
(na Windows to bude [XP ]
)
Jak to bude fungovat
Možná už sis všimla, že všechny hry, které jsme
zatím udělaly, ať už Oko bere,
šibenice, 1D piškvorky nebo Had, fungují na
podobném principu:
-
Inicializace – na začátku se
nastaví počáteční stav, například u
1D piškvorek
pole = '--------------------'
.
- A pak pořád dokola, dokud hra neskončí:
-
Vypsání stavu – aby hráč věděl,
na čem je, stav se vypíše.
-
Načtení vstupu – dostaneme od
hráče informace o tom, jak chce hrát.
-
Zpracování – na základě informací
od hráče změníme stav.
Podobný princip se používá i ve složitějších programech,
od webových serverů po textové editory,
takže stojí za to se ho držet :)
Pro nás to znamená, že si můžeme návrh programu
rozdělit na několik částí:
-
Jak popsat stav hry?
- Kdy hra končí?
-
Jak vypsat stav hry?
-
Jak načíst vstup od hráče?
-
Jak vstup zkontrolovat a aplikovat
na hru?
Pojďme si je postupně projít.
Datové struktury
První a 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.
To je stále trochu míň než má k dispozici
profesionální programátor, ale na hru s balíčkem
karet to rozhodně stačí.
Začněme od toho nejmenšího, co potřebujeme
„namodelovat”: od karty.
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
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
Dále 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]
.
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ť
python -m pytest
.
Nezapomeň mít aktivované virtuální prostředí,
kam sis před pár týdny
nainstalovala pytest.
Balíček
Jak si reprezentovat balíček či sloupec karet?
Je to nějaká sekvence karet, která může mít
různou délku (počet karet). Počet karet ve sloupci
se navíc může měnit.
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
častěji pracovat – vršek balíčku (ze kterého se
karty dobírají) nebo konec sloupce (kam se karty
přikládají nebo odkud se berou).
Sloupec C ze hry na začátku této kapitoly
by mohl být reprezentován tímto seznamem:
sloupec_c = [
(13, 'Sr', False), # Srdcový král, 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ý.
Nezapomeň použít funkci, kterou už máš napsanou!
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
Dává-li to smysl, můžeš začít psát hru!
Program
Takhle bude vypadá soubor hra.py
,
který budeme spouštět abychom si zahrály.
Funguje podle principu výše: napřed je inicializace,
a pak 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.
(To by se dalo dělat i v rámci funkce
nacti_tah
, ale, jak později zjistíme,
takhle se to bude lépe testovat.)
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 3 lícem dolů a 1 nahoru.
-
... atd.
-
Seznam sedmi sloupečků převeď na sedmici
pomocí funkce
tuple
.
-
Vytvoř dvojici balíčků
(jeden se zbytkem karet, druhý prázdný),
č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“.
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é.
A nebo pokud je ve všech cílových hromádkách po 13
kartách.
Kterou variantu použiješ ve funkci
hrac_vyhral
, to je na tobě.
Funkce ale musí brát jako argument hru,
a vracet True
nebo False
.
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á, a doma si ji pak projdi:
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-6; V: 7)
- `pocet` je počet karet, které se přemisťují
- `na` je číslo místa, kam se karty mají dát (A-G: 0-6, W-Z: 7-10)
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]))
print(popis_tahu(tah))
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, udelej_tah
, která
provede tah podle informací, které dostane z
priprav_tah
.
Bere dva argumenty:
udelej_tah(hra, info_o_tahu)
.
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
, a v případě špatného
tahu vyhodí
ValueError
s příslušnou
hláškou.
Zkus dopsat kontroly pro 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!'
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.