Uděláme karetní hru, kterou možná znáš
v některé z jejich počítačových verzí.
Vypadá takhle:
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ý.
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.
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