diff --git a/courses/pyladies.yml b/courses/pyladies.yml
index 2b014a91..5fed9b8d 100644
--- a/courses/pyladies.yml
+++ b/courses/pyladies.yml
@@ -3,7 +3,7 @@ description: Naučte se Python vážně od začátku. Žádné předchozí znalo
long_description: |
Zde najdeš materiály, které se používají na začátečnických kurzech PyLadies
- v Praze, Brně a Ostravě.
+ v Praze, Brně, Ostravě, Plzni, Olomouci nebo Hradci Králové.
Jednotlivé lekce jsou určeny naprostým začátečníkům, žádné předchozí
znalosti nejsou nutné. Instrukce jsou uvedeny pro operační systémy Linux,
@@ -15,12 +15,31 @@ sessions:
- title: Instalace
slug: install
materials:
+ - title: Studijní materiály
- lesson: beginners/cmdline
- lesson: beginners/install
- lesson: beginners/venv-setup
- lesson: beginners/first-steps
- lesson: beginners/install-editor
- lesson: git/install
+
+ - title: Videa ke shlédnutí
+ - title: (13:50) Příkazová řádka - Linux & macOS - video
+ url: https://youtu.be/GGMg8O4hE0c
+ - title: (22:34) Příkazová řádka - Windows - video
+ url: https://youtu.be/kriVWJmXpZc
+ - title: (8:15) Instalace Pythonu - Linux - video
+ url: https://youtu.be/7Qi7cSkoBA0
+ - title: (4:48) Instalace Pythonu - Windows - video
+ url: https://youtu.be/Xr6liKJzRGA
+ - title: (7:48) Nastavení prostředí - video
+ url: https://youtu.be/AFVvpfQB0V0
+ - title: (7:23) První krůčky - video
+ url: https://youtu.be/0Poe25XdKIA
+ - title: (8:01) Instalace editoru - video
+ url: https://youtu.be/JcXUCuneX04
+
+ - title: Další odkazy a materiály
- title: Tahák na klávesnici (PDF)
url: https://pyvec.github.io/cheatsheets/keyboard/keyboard-cs.pdf
type: cheatsheet
@@ -28,21 +47,56 @@ sessions:
- title: První program
slug: hello
materials:
+ - title: Studijní materiál
- lesson: beginners/hello-world
- lesson: beginners/print
- lesson: beginners/variables
- lesson: beginners/comparisons
- lesson: beginners/and-or
+ - title: Videa ke shlédnutí
+ - title: (7:50) První program - video
+ url: https://youtu.be/9aJcvOfleFs
+ - title: (12:38) Print a chybové hlášky - video
+ url: https://youtu.be/14-LVG9Edng
+ - title: (15:08) Proměnné - video
+ url: https://youtu.be/-zKws24FmCg
+ - title: (17:27) Porovnání - video
+ url: https://youtu.be/Q1YJqWzfnck
+ - title: (7:58) Nebo anebo a - video
+ url: https://youtu.be/5Mc-cgoaM10
+
+ - title: Další odkazy a materiály
+ - title: Tahák na debuggování (PDF)
+ url: https://pyvec.github.io/cheatsheets/errors/errors-cs.pdf
+ type: cheatsheet
+
- title: Cykly
slug: loops
materials:
+ - title: Studijní materiál
- lesson: beginners/expressions
- lesson: beginners/functions
- lesson: beginners/basic-functions
- lesson: intro/turtle
- lesson: beginners/while
- lesson: beginners/reassignment
+
+ - title: Videa ke shlédnutí
+ - title: (8:32) Vyhodnocování výrazů - video
+ url: https://youtu.be/nNXDXuN6Smg
+ - title: (17:26) Funkce - video
+ url: https://youtu.be/w_d8VKS8i48
+ - title: (15:51) Užitečné funkce - video
+ url: https://youtu.be/udEOvGIuZrs
+ - title: (33:02) Želva a cykly - video
+ url: https://youtu.be/A__45ibnsnc
+ - title: (6:57) Cyklus While - video
+ url: https://youtu.be/I_mkND45dB0
+ - title: (9:28) Přepisování proměnných - video
+ url: https://youtu.be/fFh1LiksgdA
+
+ - title: Další odkazy a materiály
- title: Tahák s užitečnými funkcemi
url: https://pyvec.github.io/cheatsheets/basic-functions/basic-functions-cs.pdf
type: cheatsheet
@@ -50,8 +104,11 @@ sessions:
- title: Správa zdrojového kódu
slug: git
materials:
+ - title: Studijní materiál
- lesson: git/basics
- lesson: git/branching
+
+ - title: Další odkazy a materiály
- title: Gitový tahák
url: https://pyvec.github.io/cheatsheets/basic-git/basic-git-cs.pdf
type: cheatsheet
@@ -59,10 +116,23 @@ sessions:
- title: Řetězce
slug: def-str
materials:
+ - title: Studijní materiály
- lesson: beginners/str
- lesson: beginners/str-index-slice
- lesson: beginners/str-methods
- lesson: beginners/fstring
+
+ - title: Videa ke shlédnutí
+ - title: (19:54) Zápis řetězců - video
+ url: https://youtu.be/xdZWIvryP20
+ - title: (16:12) Výběr z řetězců - video
+ url: https://youtu.be/QOamCtzLPAo
+ - title: (9:19) Řetězcové funkce a metody - video
+ url: https://youtu.be/I3CB2YihRh8
+ - title: (11:52) Šablony (formátovací řetězce) - video
+ url: https://youtu.be/2-r2e9aM1HU
+
+ - title: Další odkazy a materiály
- title: Řetězcový tahák
url: https://pyvec.github.io/cheatsheets/strings/strings-cs.pdf
type: cheatsheet
@@ -70,17 +140,40 @@ sessions:
- title: Definice funkcí
slug: def
materials:
+ - title: Studijní materiály
- lesson: beginners/def
- lesson: beginners/prefer-return
- lesson: beginners/nested-traceback
- lesson: beginners/local-variables
+ - lesson: beginners/recursion
+
+ - title: Videa ke shlédnutí
+ - title: (17:01) Definice funkcí - video
+ url: https://youtu.be/6a-RjR9fNBY
+ - title: (8:54) Vrátit nebo vypsat? - video
+ url: https://youtu.be/4M6Yq9ZyObs
+ - title: (3:58) Chybové hlášky ze zanořených funkcí - video
+ url: https://youtu.be/I_DpjBDQy_k
+ - title: (14:25) Lokální proměnné - video
+ url: https://youtu.be/gbjXnMmuAxw
+ - title: (12:23) Rekurze - video
+ url: https://youtu.be/Vu6_jK8OiAI
- title: Chyby a moduly
slug: exc
materials:
+ - title: Studijní materiály
- lesson: beginners/exceptions
- lesson: beginners/modules
# XXX when homework is added, include lesson: beginners/circular-imports
+
+ - title: Videa ke shlédnutí
+ - title: (17:11) Výjimky - video
+ url: https://youtu.be/s-P5mBjUJBw
+ - title: (10:32) Moduly - video
+ url: https://youtu.be/LFp_x7OgCBU
+
+ - title: Další odkazy a materiály
- title: Výjimkový tahák
url: https://pyvec.github.io/cheatsheets/exceptions/exceptions-cs.pdf
type: cheatsheet
@@ -88,15 +181,30 @@ sessions:
- title: Rozhraní a testy
slug: tests
materials:
+
+ - title: Studijní materiály
- lesson: beginners/interfaces
- lesson: beginners/testing
- lesson: beginners/main-module
+ - title: Videa ke shlédnutí
+ - title: (6:51) Rozhraní - video
+ url: https://youtu.be/xQ7J_R8VAJQ
+ - title: (15:25) Testování - video
+ url: https://youtu.be/-KS_VRerZQ0
+ - title: (10:12) Spouštěcí moduly - video
+ url: https://youtu.be/S6HyFIUyPTw
+ - title: (3:53) Negativní testy - video
+ url: https://youtu.be/s-KPxX5ZcMs
+
- title: Spolupráce a Open-Source
slug: foss
materials:
+ - title: Studijní materiály
- lesson: git/collaboration
- lesson: git/ignoring
+
+ - title: Další odkazy a materiály
- title: Gitový tahák
url: https://pyvec.github.io/cheatsheets/basic-git/basic-git-cs.pdf
type: cheatsheet
@@ -104,51 +212,116 @@ sessions:
- title: Seznamy
slug: list
materials:
+ - title: Studijní materiály
- lesson: beginners/list
- lesson: beginners/tuple
- lesson: beginners/nested-list
+
+ - title: Videa ke shlédnutí
+ - title: (11:10) Seznamy - Úvod - video
+ url: https://youtu.be/abIMJjb9Nxw
+ - title: (32:52) Co všechno umí seznamy - video
+ url: https://youtu.be/-gINX_pOtQo
+ - title: (9:51) N-tice - video
+ url: https://youtu.be/dObtA75Ouzk
+
+
+ - title: Další odkazy a materiály
- title: Tahák na seznamy
url: https://pyvec.github.io/cheatsheets/lists/lists-cs.pdf
type: cheatsheet
+ - lesson: beginners/vscode
- title: Sekvence a soubory
slug: seq
materials:
+ - title: Studijní materiály
- lesson: beginners/range
- lesson: beginners/zip-enumerate
- lesson: beginners/files
- lesson: beginners/with
-- title: Grafika
- slug: pyglet
- materials:
- - lesson: intro/pyglet
- - lesson: projects/pong
- - title: Tahák na Pyglet
- url: https://pyvec.github.io/cheatsheets/pyglet/pyglet-basics-cs.pdf
- type: cheatsheet
+ - title: Videa ke shlédnutí
+ - title: (5:02) Range - video
+ url: https://youtu.be/UtyIKgsnLmk
+ - title: (16:39) Iterátory n-tic (enum & zip) - video
+ url: https://youtu.be/JUCb7iiGsW4
+ - title: (13:17) Soubory - video
+ url: https://youtu.be/1X4TU_thg7s
- title: Slovníky
slug: dict
materials:
+ - title: Studijní materiály
- lesson: beginners/dict
- - lesson: intro/json
- - lesson: projects/github-api
- lesson: beginners/dict-with-list-values
+
+ - title: Videa ke shlédnutí
+ - title: (19:23) Slovníky - video
+ url: https://youtu.be/OVnUhWwd2C0
+ - title: (4:42) Více hodnot v jednom záznamu slovníku - video
+ url: https://youtu.be/pQ268T2IZCo
+
+ - title: Další odkazy a materiály
- title: Slovníkový tahák
url: https://pyvec.github.io/cheatsheets/dicts/dicts-cs.pdf
type: cheatsheet
+- title: JSON a API
+ slug: json
+ materials:
+ - lesson: intro/json
+ - lesson: projects/github-api
+ - title: Ukázka jednoduchého API
+ type: lesson
+ url: http://pyladies.cz/v1/s011-dicts/simple-api.html
+
+ - title: Videa ke shlédnutí
+ - title: (12:54) Kódování dat - JSON - video
+ url: https://youtu.be/nPUSH7gSI4o
+
+ - title: Další odkazy a materiály
+ - title: Co je API? (externí materiál)
+ url: https://cojeapi.cz
+
- title: Třídy
slug: class
materials:
+ - title: Studijní materiály
- lesson: beginners/class
- lesson: beginners/inheritance
+ - title: Videa ke shlédnutí
+ - title: (22:26) Třídy - video
+ url: https://youtu.be/x4iJDPOSv4A
+ - title: (12:58) Dědičnost - video
+ url: https://youtu.be/mqxMmSFiASw
+
+- title: Grafika
+ slug: pyglet
+ materials:
+ - title: Studijní materiály
+ - lesson: intro/pyglet
+ - lesson: projects/pong
+ - title: Kód celé hry Pong
+ url: http://pyladies.cz/v1/s012-pyglet/pong.py
+
+ - title: Další odkazy a materiály
+ - title: Tahák na Pyglet
+ url: https://pyvec.github.io/cheatsheets/pyglet/pyglet-basics-cs.pdf
+ type: cheatsheet
+
- title: Závěrečný projekt
slug: asteroids
materials:
+ - title: Studijní materiály
- lesson: projects/asteroids
+ - lesson: projects/snake
+ - lesson: klondike/cards
+ - lesson: klondike/decks
+ - lesson: klondike/game
+
+ - title: Další odkazy a materiály
- title: Množinový tahák
url: https://pyvec.github.io/cheatsheets/sets/sets-cs.pdf
type: cheatsheet
diff --git a/lessons/beginners/vscode/index.md b/lessons/beginners/vscode/index.md
new file mode 100644
index 00000000..e9c1e0d8
--- /dev/null
+++ b/lessons/beginners/vscode/index.md
@@ -0,0 +1,117 @@
+# Visual Studio Code & Python
+
+Visual Studio Code (také VSCode) je editor zdrojového kódu vyvíjený společností Microsoft pro operační systémy Windows, Linux a macOS. (viz oficiální web: [https://code.visualstudio.com/](https://code.visualstudio.com/) )
+
+## Nastavení VSCode
+
+VSCode umožňuje nastavit spoustu věcí a jelikož pracuji na Windows, budu se bavit hlavně o Windows, ale postupy by měly být totožné i v jiných operačních systémech.
+
+Jedním ze základním příkazů ve VSCode je otevření příkazové řádky (pozor, jedná se o příkazovou řádku ve VSCode, neplést s příkazovou řádkou ve Winsows). Tato příkazová řádka umožňuje vykonávat různé operace, které jdou vyvolat i jinými způsoby (např. z menu), ale je snazší ji používat, protože umožňuje i textové vyhledávání. Vyvoláme ji jednám z následujících způsobů:
+
+- z horního menu: `View > Command Pallete...`
+- klávesová zkr.: `F1`
+- klávesová zkr.: `CTRL+SHIFT+P` (na Macu `CMD+SHIFT+P`)
+
+a po otevření vypadá následovně
+
+
+
+### Nastavení terminálu
+
+VSCode umožňuje nastavit terminál v jakém budeme pracovat, nastavíme jej následovně:
+
+1. Otevřeme VSCode příkazový řádek
+1. Napíšeme `>Terminal: Select Default Profile`
+(nebo jen část. Pozor, znak `>` na začátku je důležitý - nemazat)
+
+ 
+
+1. A vybereme preferovaný terminál - na Windows doporučuju `Command Prompt`, pro jednoduchost a kompatibilitu s kurzy.
+
+ 
+
+### Instalace rozšíření
+
+VSCode umožňuje instalovat spoustuužitečných rozšíření a vytvořit tak z jednoduchého textového editoru velmi mocný nástroj.
+
+Pro naši potřebu budeme potřebovat rozšíření `Python` (od Microsoftu), které nainstalujeme tak, že z levého panelu vybereme ikonu s čtverečky, vyhledávacího pole zadáme `Python` a klikneme na tlačítko `Install`.
+
+
+
+**Doporučení:** Pokud používáte svůj počítač, máte nainstalováno více rozšíření a víte, že jste je neinstalovali, je dobré je buď zakázat nebo rovnou odstranit - můžou způsobovat nepříjemnosti a zpomalovat celý VSCode.
+
+## Práce s Pythonem ve VSCode
+
+### Vytvoření virtuálního prostředí
+
+1. Otevřete ve VSCode prázdnou složku (nebo složku bez virtuálního prostředí) - složka ve VSCode představuje něco jako projekt, obsahuje zdrojové soubory, ale ukládají se zde i různá nastavení, ...
+1. Otevřete `Příkazovou řádku` ve VSCode (kláv. zkr: `F1`) a vyberte příkaz `>Python: Create Environment` (stačí napsat kousek, viz obrázek) a spustí se drobný průvodce, který vám usnadní vytvořit prostředí (viz jednotlivé kroky níže)
+
+ 
+
+ - Výběr virtuálního prostředí (v kurzech používáme `Venv`)
+
+ 
+
+ - výběr verze Pythonu (stačí vybrat doporučenou verzi)
+
+ 
+
+ V mé prázdné složce (`vscode_test`) se vytvoří virtuální prostředí (ve složce `.venv`)
+
+ 
+
+1. Koukneme, jestli se prostředí i aktivovalo...
+
+ - otevřeme si terminál
+
+ - pomocí klávesové zkratky `CTRL+;` (na Macu `CMD+;`) - pomocí této zkratky jde i skrývat a znovu zobrazovat
+ - z menu `View > Terminal`
+
+ Otevře se nový terminál s aktivovaným prostředím
+
+ 
+
+ Pokud chceme terminál odstranit, použijme ikonu koše (okno Terminálu v pravo nahoře) - ikona křížku terminál pouze schová.
+
+ 
+
+ - ověřit aktivní prostředí jde i ve spodním panelu, kde se ukáže, jakou verzi pythonu používáme, a že je opravdu z naší složky `.venv`
+
+ 
+
+### Pytest
+
+Python rozšíření taky celkem dobře integruje testování do VSCode. Zprovozníme jej následovně:
+
+1. Otevřeme `VSCode příkazovou řádku` (`F1`) a zadáme příkaz `>Python: Configure Tests` (stčí napsat část, viz obr)
+
+ 
+
+1. Vybereme testovací framework, který chceme použít (v našem případě `pytest`)
+
+ 
+
+1. Vybert složku, kde jsou (či budou) umístěny testy (v našem případě `.` - hlavní složka projektu). Pytest bude testy hledat pouze v této složce a jejich potomcích.
+
+ 
+
+To je vše - Pytest je nainstalován, nastaven a připraven k použití. VSCode si taky uložilo nějaké nastavení (které jsme mu před chvílí zadali) v naší složce (projektu), které se znova načte při otevření této složky.
+
+ 
+
+Jak Testy spustit? Jednouše, mámě několik možností
+
+- přímo z editoru
+
+ 
+
+- ze speciálního panelu pro testy
+
+ 
+
+Ukázka chyby
+
+ 
+
+Poznámka: Samozřejmě můžeme testy pustit i z Terminálu ;-).
diff --git a/lessons/beginners/vscode/info.yml b/lessons/beginners/vscode/info.yml
new file mode 100644
index 00000000..ad0f9d7d
--- /dev/null
+++ b/lessons/beginners/vscode/info.yml
@@ -0,0 +1,4 @@
+title: VS Code & Python
+style: md
+attribution: Pro PyLadies Ostrava napsal Michal Vašut, 2022.
+license: cc-by-sa-40
diff --git a/lessons/beginners/vscode/static/cmd-cmd.png b/lessons/beginners/vscode/static/cmd-cmd.png
new file mode 100644
index 00000000..985ccad5
Binary files /dev/null and b/lessons/beginners/vscode/static/cmd-cmd.png differ
diff --git a/lessons/beginners/vscode/static/cmd-pallete.png b/lessons/beginners/vscode/static/cmd-pallete.png
new file mode 100644
index 00000000..fcef64a3
Binary files /dev/null and b/lessons/beginners/vscode/static/cmd-pallete.png differ
diff --git a/lessons/beginners/vscode/static/cmd-terminal.png b/lessons/beginners/vscode/static/cmd-terminal.png
new file mode 100644
index 00000000..8aa75b3a
Binary files /dev/null and b/lessons/beginners/vscode/static/cmd-terminal.png differ
diff --git a/lessons/beginners/vscode/static/create-venv.png b/lessons/beginners/vscode/static/create-venv.png
new file mode 100644
index 00000000..327eb03f
Binary files /dev/null and b/lessons/beginners/vscode/static/create-venv.png differ
diff --git a/lessons/beginners/vscode/static/extension.png b/lessons/beginners/vscode/static/extension.png
new file mode 100644
index 00000000..55982e0c
Binary files /dev/null and b/lessons/beginners/vscode/static/extension.png differ
diff --git a/lessons/beginners/vscode/static/new_venv.png b/lessons/beginners/vscode/static/new_venv.png
new file mode 100644
index 00000000..17d39b13
Binary files /dev/null and b/lessons/beginners/vscode/static/new_venv.png differ
diff --git a/lessons/beginners/vscode/static/pytest-1.png b/lessons/beginners/vscode/static/pytest-1.png
new file mode 100644
index 00000000..ce1c3b48
Binary files /dev/null and b/lessons/beginners/vscode/static/pytest-1.png differ
diff --git a/lessons/beginners/vscode/static/pytest-2.png b/lessons/beginners/vscode/static/pytest-2.png
new file mode 100644
index 00000000..c6b73b74
Binary files /dev/null and b/lessons/beginners/vscode/static/pytest-2.png differ
diff --git a/lessons/beginners/vscode/static/pytest-error.png b/lessons/beginners/vscode/static/pytest-error.png
new file mode 100644
index 00000000..3ff213dd
Binary files /dev/null and b/lessons/beginners/vscode/static/pytest-error.png differ
diff --git a/lessons/beginners/vscode/static/pytest.png b/lessons/beginners/vscode/static/pytest.png
new file mode 100644
index 00000000..18e553cd
Binary files /dev/null and b/lessons/beginners/vscode/static/pytest.png differ
diff --git a/lessons/beginners/vscode/static/terminal-kill.png b/lessons/beginners/vscode/static/terminal-kill.png
new file mode 100644
index 00000000..464f9322
Binary files /dev/null and b/lessons/beginners/vscode/static/terminal-kill.png differ
diff --git a/lessons/beginners/vscode/static/terminal.png b/lessons/beginners/vscode/static/terminal.png
new file mode 100644
index 00000000..a0717b15
Binary files /dev/null and b/lessons/beginners/vscode/static/terminal.png differ
diff --git a/lessons/beginners/vscode/static/tests-config.png b/lessons/beginners/vscode/static/tests-config.png
new file mode 100644
index 00000000..d535bfca
Binary files /dev/null and b/lessons/beginners/vscode/static/tests-config.png differ
diff --git a/lessons/beginners/vscode/static/tests-folder.png b/lessons/beginners/vscode/static/tests-folder.png
new file mode 100644
index 00000000..a9bc163f
Binary files /dev/null and b/lessons/beginners/vscode/static/tests-folder.png differ
diff --git a/lessons/beginners/vscode/static/tests-settings.png b/lessons/beginners/vscode/static/tests-settings.png
new file mode 100644
index 00000000..09a11e8f
Binary files /dev/null and b/lessons/beginners/vscode/static/tests-settings.png differ
diff --git a/lessons/beginners/vscode/static/venv-panel.png b/lessons/beginners/vscode/static/venv-panel.png
new file mode 100644
index 00000000..22728e3b
Binary files /dev/null and b/lessons/beginners/vscode/static/venv-panel.png differ
diff --git a/lessons/beginners/vscode/static/venv-python.png b/lessons/beginners/vscode/static/venv-python.png
new file mode 100644
index 00000000..9b870da7
Binary files /dev/null and b/lessons/beginners/vscode/static/venv-python.png differ
diff --git a/lessons/beginners/vscode/static/venv-type.png b/lessons/beginners/vscode/static/venv-type.png
new file mode 100644
index 00000000..850351f3
Binary files /dev/null and b/lessons/beginners/vscode/static/venv-type.png differ
diff --git a/lessons/klondike/cards/index.md b/lessons/klondike/cards/index.md
new file mode 100644
index 00000000..acdc808c
--- /dev/null
+++ b/lessons/klondike/cards/index.md
@@ -0,0 +1,349 @@
+# Klondike Solitaire: Karty
+
+Pojďme vytvořit karetní hru *Klondike Solitaire*, kterou možná znáš v nějaké
+počítačové verzi.
+
+{{ figure(img=static('klondike.png'), alt="Jedna z grafických podob hry") }}
+
+Naše hra bude ze začátku jednodušší – nebudeme se zabývat grafikou,
+ale logikou hry.
+„Grafiku“ zatím zajistí textová konzole. Obrázek výše se dá ukázat jako:
+
+```plain
+ U V W X Y Z
+ [???] [ ] [ ] [ ] [ ] [ ]
+
+ A B C D E F G
+ [3♣ ] [???] [???] [???] [???] [???] [???]
+ [5 ♥] [???] [???] [???] [???] [???]
+ [6♣ ] [???] [???] [???] [???]
+ [5♠ ] [???] [???] [???]
+ [Q ♥] [???] [???]
+ [4♠ ] [???]
+ [3 ♦]
+```
+
+
+## Pasiáns
+
+*Klondike Solitaire* je [pasiáns](https://cs.wikipedia.org/wiki/Pasi%C3%A1ns)
+– karetní hra pro jednoho hráče.
+Tyto hry obecně fungují takto:
+
+* Karty se určitým způsobem *rozdají* do několika balíčků, hromádek nebo
+ jiných skupin
+* Dokud hráč *nevyhrál*:
+ * Hráč *udělá tah*: podle určitých pravidel přesune karty z jedné hromádky
+ na druhou
+
+Než ale počítač naučíš hrát hru, je potřeba ho naučit pár základních věcí,
+aby pak instrukce pro samotnou hru dávaly smysl.
+Základní věci, které je potřeba počítač „naučit“, jsou:
+
+* Co je to *karta*?
+* Co je to *balíček*?
+
+Odpovědí na tyhle otázky bude spousta vysvětlování a taky několik pythonních
+funkcí, které použiješ i ve zbytku hry.
+
+Tady bych rád podotknul, že tyhle materiály ukazují předem vyzkoušený způsob,
+jak napsat karetní hru.
+Reálné projekty takhle nefungují: zahrnují spoustu plánování, slepých uliček,
+oprav špatně navrženého kódu a jiných frustrací.
+Neděláme tu reálný softwarový projekt – zatím stále *zkoušíme základy*,
+jen z nich pak vyleze něco hezkého.
+
+
+## Karta a balíček
+
+Co je to *karta* a *balíček*?
+Jak tyhle koncepty *reprezentovat* v Pythonu – tedy pomocí čísel, řetězců,
+seznamů, n-tic a jiných datových typů – abys s nimi mohl{{a}}
+dál pracovat?
+
+Možností jak to udělat je více.
+Dobrý návrh *datového modelu* je základ úspěšného projektu: odpověď na otázku
+výše je základ k tomu, aby se pak program hezky psal.
+Až budeš potřebovat dobrý návrh datového modelu pro nějaký svůj projekt,
+doporučuju se ze začátku poradit se zkušenějším programátorem.
+
+Pro Solitaire je tento úkol za tebe vyřešený: hrou Klondike si procvičíš
+seznamy a n-tice (a později slovníky).
+
+
+### Karta
+
+O *kartě* potřebuješ znát tři kousky informace: hodnotu, barvu a to, jestli
+je otočená rubem nebo lícem nahoru.
+
+*Hodnoty* karet jsou čísla 2-10 a navíc `J`, `Q`, `K`, `A`.
+Hodnoty „obrázkových“ karet je dobré převést na čísla: J=11, Q=12, K=14, A=1.
+Hodnoty se tak budou dát jednoduše porovnávat, nebo zjišťovat následující kartu
+(např. po desítce je jedenáct – `J`).
+V programu budeme tedy pro hodnoty používat jen čísla, a teprve když bude
+potřeba kartu „ukázat“ člověku, převedeme ji na písmenko.
+
+Pro *barvu* jsou čtyři možnosti: ♥, ♦, ♣ nebo ♠.
+Dají se reprezentovat v podstatě jakýmikoli čtyřmi různými hodnotami.
+Různí programátoři by mohli použít čísla 0-3, symboly jako `♥`, nebo třeba jako
+čtyři různé funkce.
+My použijeme krátké řetězce bez diakritiky, aby se to dobře psalo:
+`'Sr'` (srdce), `'Pi'` (piky), `'Ka'` (káry), `'Kr'` (kříže).
+Použij prosím stejné řetězce (včetně velkých písmen), abys pak mohl{{a}}
+kopírovat ukázkový kód.
+Jako u hodnoty platí že tyhle řetězce budeme používat ve většině programu,
+jen když bude potřeba kartu „ukázat“ člověku, převedeme je na hezčí symbol.
+
+Pro *otočení* karty jsou dvě možné hodnoty: buď lícem nebo rubem nahoru.
+Když dvě hodnoty, je dobré použít `True` a `False`.
+Jen je pak potřeba vybrat (a dodržovat) která je která.
+Řekněme že `True` znamená lícem nahoru; `False` rubem.
+Ideální je podle toho důsledně pojmenovávat proměnné: v programu vždy
+používej `je_licem_nahoru=True`, ne `otoceni=True`.
+
+Samotná karta pak bude trojice těchto hodnot: (hodnota, barva, je_licem_nahoru).
+Například:
+
+* `(12, 'Sr', True)` je 🂽 – srdcová královna otočená lícem nahoru
+* `(7, 'Pi', False)` je 🂠 – piková sedma otočená rubem nahoru
+
+
+### Balíček
+
+A balíček? Balíček bude seznam karet, tedy seznam trojic.
+Jakákoli sekvence karet ve hře bude bude seznam trojic: dobírací a odkládací
+balíčky, „sloupečky“ karet na herní ploše i „hromádky“ sežazených karet.
+
+Například jeden ze sloupečků z obrázku výše obsahuje 4 karty rubem nahoru
+a na konci srdcovou královnu.
+Jako seznam by to mohlo být:
+
+```python
+[(7, 'Pi', False), (5, 'Kr', False), (1, 'Ka', False), (3, 'Pi', False), (12, 'Sr', True)]
+```
+
+
+### Seznamy a n-tice
+
+Na balíčcích a kartách je vidět rozdíl v použití seznamů a n-tic:
+
+* N-tice má pevně dané N: karta je trojice, ne čtveřice
+ ani dvojice.
+ Oproti tomu seznamy nemívají pevně danou délku: hromádka karet může být velká,
+ malá, nebo dokonce prázdná.
+ Dokonce může během hry růst nebo se zmenšovat, třeba když si „lízneš“ kartu
+ nebo balíček rozdělíš na dvě části.
+* Seznamy často dává smysl zamíchat nebo seřadit.
+ Když zamíchám balíček karet, je to stále baliček karet.
+ Když ale zamíchám pořadím prvků ve trojici *(hodnota, barva, je_licem_nahoru)*,
+ bude to sice pořád trojice, ale už to nejspíš nebude *karta*.
+* S tím souvisí to, že v seznamy bývají tzv. *homogenní*: každý prvek stejný
+ typ. Máme balíček karet (trojic), ale karty jsou trojice
+ (číslo, řetězec, pravdivostní hodnota).
+
+> [note]
+> Ne ve všech programech to bude takhle jednoznačné. Karta a balíček jsou
+> skoro ideální příklady na seznamy a n-tice :)
+
+V Pythonu z použitých typů vyplývá, co se s nimi dá dělat.
+
+N-tice nejdou měnit: abys změnil{{a}} např. otočení karty, bude
+potřeba udělat úplně novou trojici (podobně jako např u tahu
+z `--------------------` na `-------------o------` v 1D piškvorkách).
+
+Seznamy ale měnit jdou. Seznamové operace dokonce často dávají smysl:
+
+* *append* je přiložení karty na vršek hromádky.
+* *pop* je líznutí karty (z balíčku zmizí, ale karta zůstane v ruce – jako návratová hodnota).
+* *extend* je přidání jednoho balíčku ke druhému.
+* *random.shuffle* je zamíchání karet.
+* *sort* je seřazení karet.
+
+Pozor ale na to, že se seznamem trojic toho jde dělat víc než s fyzickým
+balíčkem karet.
+Pro počítač není problém udělat kopii karty.
+
+
+## Pomocné funkce
+
+Označovat dsrdcovou dámu jako `(12, 'Sr', True)` je skvělé pro počítač,
+ale pro lidi je to nepřehledné.
+Bude tedy vhodné napsat funkci, která kartu „ukáže“ trochu srozumitelněji.
+Taková funkce by měla vyřeši i to, že kartu, která je rubem nahoru
+– jako `(5, 'Kr', False)`, je potřeba před hráčem skrýt.
+
+Napsat tuhle funkci je docela otrava, a později bude potřeba
+aby se chovala *přesně* podle mých očekávání
+(včetně např. velkých písmen a mezer).
+Proto ti ji dám k dispozici. Hlavičku má takovouhle:
+
+```python
+def popis_kartu(karta):
+ """Vrátí popis karty, např. [Q ♥] nebo [6♣ ] nebo [???]
+
+ Trojice čísla (2-13), krátkého řetězce ('Sr', 'Ka', 'Kr' nebo 'Pi')
+ a logické hodnoty (True - lícem nahoru; False - rubem) se jednoduše
+ zpracovává v Pythonu, ale pro "uživatele" není nic moc.
+ Proto je tu tahle funkce, která kartu hezky "popíše".
+
+ Aby měly všechny hodnoty jen jeden znak, desítka se vypisuje jako
+ římská číslice "X".
+
+ Aby se dalo rychle odlišit červené (♥♦) karty od černých (♣♠),
+ mají červené mezeru před symbolem a černé za ním.
+ """
+```
+
+Druhá užitečná funkce umí otočit karu buď rubem nebo lícem nahoru.
+Podobně jako `tah` z piškvorek vezme „starou“ hodnotu, rozloží ji
+části a výsledek slepí z kombinace „starých“ a „nových“ hodnot.
+
+Projdi si ji řádek po řádku, abys věděl{{a}} jak funguje:
+
+```python
+def otoc_kartu(karta, pozadovane_otoceni):
+ """Vrátí kartu otočenou lícem nahoru (True) nebo rubem nahoru (False)
+
+ Nemění původní trojici; vytvoří a vrátí novou.
+ (Ani by to jinak nešlo – n-tice se, podobně jako řetězce čísla, měnit
+ nedají.)
+ """
+
+ # Rozbalení n-tice
+ hodnota, barva, stare_otoceni = karta
+
+ # Vytvoření nové n-tice (kombinací staré hodnoty/barvy a nového otočení)
+ nova_karta = hodnota, barva, pozadovane_otoceni
+
+ # Vrácení nové n-tice
+ return nova_karta
+```
+
+Funkce najdeš v souboru [`karty.py`]. Projdi si je; rozumíš jim?
+
+Testy k nim jsou v [`test_karty.py`] – ty procházet nemusíš.
+
+[`karty.py`]: {{ static('karty.py') }}
+[`test_karty.py`]: {{ static('test_karty.py') }}
+
+Oba soubory si ulož.
+
+
+### Testy a úkoly
+
+Další pomocné funkce už napíšeš {{gnd('sám', 'sama')}}.
+Aby sis ověřil{{a}} že fungují, mám pro tebe předpřipravené testy.
+
+Stáhni si soubor s testy, [test_klondike.py], a dej ho do adresáře,
+kde budeš tvořit hru a kde máš `karty.py`.
+
+Na ulehčení testování si nainstaluj modul `pytest-level`.
+Ten umožňuje pouštět jen určité testy – podle toho, jak jsi daleko.
+
+ python -m pip install pytest pytest-level
+
+Zkus pustit všechny testy. Asi ti neprojdou:
+
+ python -m pytest -v
+
+Pak zkus pustit testy pro úroveň 0:
+
+ python -m pytest -v --level 0
+
+Teď se nepustí žádné testy – všechny nové testy se přeskočí;
+projdou jen testy z `test_karty.py`.
+Uvidíš něco jako:
+
+```pytest
+===== 20 passed, 99 deselected in 0.06s ====
+```
+
+Zadáš-li v posledním příkazu `--level 1`, aktivuje se první z testů. Pravděpodobně neprojde – v dalším úkolu ho spravíš!
+
+[test_klondike.py]: {{ static('test_klondike.py') }}
+
+
+
+### Vytvoření balíčku
+
+Do modulu `klondike` (tedy do nového souboru `klondike.py`) napiš
+následující funkci:
+
+```python
+def vytvor_balicek():
+ """Vrátí balíček 52 karet – od esa (1) po krále (13) ve čtyřech barvách
+
+ Všechny karty jsou otočené rubem nahoru.
+ """
+```
+
+Zkus si funkci pustit a podívej se, co vrací.
+
+Aby sis ověřil{{a}}, že se chová správně, pusť na ni testy:
+
+* level 10: Funkce existuje
+* level 11: V balíčku je 52 karet, žádné se neopakují.
+* level 12: V balíčku jsou všechny požadované karty.
+* level 13: Balíček je zamíchaný.
+
+
+### Rozepsání balíčku
+
+Když výsledek funkce `vytvor_balicek` vypíšeš, je docela nepřehledný.
+Aby se ti s balíčky lépe pracovalo, vytvoř následující funkci:
+
+```python
+def popis_balicek(karty):
+ """Vrátí popis všech karet v balíčku. Jednotlivé karty odděluje mezerami.
+ """
+```
+
+Funkce by měla vracet řetězec složený z popisů jednotlivých karet.
+Například:
+
+```pycon
+>>> karty = [
+ (13, 'Pi', True),
+ (12, 'Sr', True),
+ (11, 'Ka', True),
+ (10, 'Kr', False),
+ ]
+
+>>> popis_balicek(karty)
+[K♠ ] [Q ♥] [J ♦] [???]
+```
+
+Jak na to?
+Vytváření celého řetězce najednou by bylo složité, ale lze si to rozdělit
+na kroky, které už znáš:
+
+* Vytvoř prázdný seznam.
+* Postupně do senamu přidej popisy jednotlivých karet.
+ (Využij funkci `popis_kartu` z modulu `karty`!)
+* Vrať popisky oddělené mezerami. (Koukni na tahák k seznamům!)
+
+Funkci opět můžeš otestovat:
+
+* level 20: Funkce existuje
+* level 21: Funkce správně popisuje balíček
+* level 22: Funkce umí popsat i prázdný balíček
+
+
+### Popis balíčku
+
+Občas je z balíčku vidět jen vrchní karta.
+Napiš následující funkci, která popíše takový balíček:
+
+```python
+def popis_vrchni_kartu(balicek):
+ """Vrátí popis daného balíčku karet -- tedy vrchní karty, která je vidět."""
+```
+
+Funkci nezapomeň otestovat:
+
+* level 30: Funkce existuje
+* level 31: Funkce vrátí popis poslední karty. (Bude se hodit funkce `popis_kartu` z modulu `karty`.)
+* level 32: Funkce popíše prázdný balíček jako `[ ]` (3 mezery v hranatých závorkách).
+
+
+Pokračování příště...
diff --git a/lessons/klondike/cards/info.yml b/lessons/klondike/cards/info.yml
new file mode 100644
index 00000000..4c830b68
--- /dev/null
+++ b/lessons/klondike/cards/info.yml
@@ -0,0 +1,4 @@
+title: "Klondike: Karty"
+style: md
+attribution: Pro PyLadies Brno napsal Petr Viktorin, 2014-2019.
+license: cc-by-sa-40
diff --git a/lessons/klondike/cards/static/karty.py b/lessons/klondike/cards/static/karty.py
new file mode 100644
index 00000000..cb516666
--- /dev/null
+++ b/lessons/klondike/cards/static/karty.py
@@ -0,0 +1,69 @@
+"""Základní operace s "kartou" - trojicí (hodnota, barva, je_licem_nahoru)
+"""
+
+def popis_kartu(karta):
+ """Vrátí popis karty, např. [Q ♥] nebo [6♣ ] nebo [???]
+
+ Trojice čísla (2-13), krátkého řetězce ('Sr', 'Ka', 'Kr' nebo 'Pi')
+ a logické hodnoty (True - lícem nahoru; False - rubem) se jednoduše
+ zpracovává v Pythonu, ale pro "uživatele" není nic moc.
+ Proto je tu tahle funkce, která kartu hezky "popíše".
+
+ Aby měly všechny hodnoty jen jeden znak, desítka se vypisuje jako
+ římská číslice "X".
+
+ Aby se dalo rychle odlišit červené (♥♦) karty od černých (♣♠),
+ mají červené mezeru před symbolem a černé za ním.
+ """
+
+ # Rozbalení n-tice, abychom mohli pracovat s jednotlivými složkami
+ hodnota, barva, je_licem_nahoru = karta
+
+ # Když je vidět jen rub, rovnou vrátíme [???]
+ if not je_licem_nahoru:
+ return '[???]'
+
+ # Popis hodnoty: pár speciálních případů, plus čísla 2-9
+ if hodnota == 11:
+ popis_hodnoty = 'J'
+ elif hodnota == 12:
+ popis_hodnoty = 'Q'
+ elif hodnota == 13:
+ popis_hodnoty = 'K'
+ elif hodnota == 1:
+ popis_hodnoty = 'A'
+ elif hodnota == 10:
+ popis_hodnoty = 'X'
+ else:
+ popis_hodnoty = str(hodnota)
+
+ # Popis barvy: čtyři možnosti
+ if barva == 'Sr':
+ popis_barvy = ' ♥'
+ elif barva == 'Ka':
+ popis_barvy = ' ♦'
+ elif barva == 'Kr':
+ popis_barvy = '♣ '
+ elif barva == 'Pi':
+ popis_barvy = '♠ '
+
+ # Vrácení hodnoty
+ return f'[{popis_hodnoty}{popis_barvy}]'
+
+
+def otoc_kartu(karta, pozadovane_otoceni):
+ """Vrátí kartu otočenou lícem nahoru (True) nebo rubem nahoru (False)
+
+ Nemění původní trojici; vytvoří a vrátí novou.
+ (Ani by to jinak nešlo – n-tice se, podobně jako řetězce čísla, měnit
+ nedají.)
+ """
+
+ # Rozbalení n-tice
+ hodnota, barva, stare_otoceni = karta
+
+ # Vytvoření nové n-tice (kombinací staré hodnoty/barvy a nového otočení)
+ nova_karta = hodnota, barva, pozadovane_otoceni
+
+ # Vrácení nové n-tice
+ return nova_karta
diff --git a/lessons/klondike/cards/static/klondike.png b/lessons/klondike/cards/static/klondike.png
new file mode 100644
index 00000000..61d321e4
Binary files /dev/null and b/lessons/klondike/cards/static/klondike.png differ
diff --git a/lessons/klondike/cards/static/test_karty.py b/lessons/klondike/cards/static/test_karty.py
new file mode 100644
index 00000000..aff5a74c
--- /dev/null
+++ b/lessons/klondike/cards/static/test_karty.py
@@ -0,0 +1,70 @@
+import pytest
+import karty
+
+
+def test_popis_rubem_nahoru():
+ """Popis karty, která je rubem nahoru, by měl ukázat otazníky"""
+ karta = 13, 'Pi', False
+ assert karty.popis_kartu(karta) == '[???]'
+
+
+def test_popis_srdcova_kralovna():
+ """Popis srdcové královny by měl být "[Q ♥]"."""
+ karta = 12, 'Sr', True
+ assert karty.popis_kartu(karta) == '[Q ♥]'
+
+
+def test_popis_krizova_sestka():
+ """Popis křížové šestky by měl být "[6♣ ]"."""
+ karta = 6, 'Kr', True
+ assert karty.popis_kartu(karta) == '[6♣ ]'
+
+
+def test_popis_karova_desitka():
+ """Popis kárové desítky by měl být "[X ♦]"."""
+ karta = 10, 'Ka', True
+ assert karty.popis_kartu(karta) == '[X ♦]'
+
+
+def test_popis_pikove_eso():
+ """Popis pikového esa by měl být "[A♠ ]"."""
+ karta = 1, 'Pi', True
+ assert karty.popis_kartu(karta) == '[A♠ ]'
+
+
+def test_otoc_kralovnu():
+ """Kontrola otočení karty, co je na začátku lícem nahoru"""
+ karta = 12, 'Sr', True
+ assert karty.otoc_kartu(karta, True) == (12, 'Sr', True)
+ assert karty.otoc_kartu(karta, False) == (12, 'Sr', False)
+
+
+def test_otoc_eso():
+ """Kontrola otočení karty, co je na začátku rubem nahoru"""
+ karta = 1, 'Pi', False
+ assert karty.otoc_kartu(karta, True) == (1, 'Pi', True)
+ assert karty.otoc_kartu(karta, False) == (1, 'Pi', False)
+
+
+# Tohle je testovací vychytávka, kterou zatím neznáme:
+# několik podobných testů zadaných jednou funkcí
+PRIKLADY = [
+ (1, 'Ka', '[A ♦]'),
+ (2, 'Ka', '[2 ♦]'),
+ (3, 'Sr', '[3 ♥]'),
+ (4, 'Sr', '[4 ♥]'),
+ (5, 'Kr', '[5♣ ]'),
+ (6, 'Pi', '[6♠ ]'),
+ (7, 'Ka', '[7 ♦]'),
+ (8, 'Kr', '[8♣ ]'),
+ (9, 'Sr', '[9 ♥]'),
+ (10, 'Kr', '[X♣ ]'),
+ (11, 'Ka', '[J ♦]'),
+ (12, 'Sr', '[Q ♥]'),
+ (13, 'Kr', '[K♣ ]'),
+]
+@pytest.mark.parametrize(['hodnota', 'barva', 'pozadovany_popis'], PRIKLADY)
+def test_popis_hodnoty(hodnota, barva, pozadovany_popis):
+ """Kontrola popisu karty"""
+ karta = hodnota, barva, True
+ assert karty.popis_kartu(karta) == pozadovany_popis
diff --git a/lessons/klondike/cards/static/test_klondike.py b/lessons/klondike/cards/static/test_klondike.py
new file mode 100644
index 00000000..731d11db
--- /dev/null
+++ b/lessons/klondike/cards/static/test_klondike.py
@@ -0,0 +1,824 @@
+import pytest
+import textwrap
+import re
+
+@pytest.mark.level(1)
+def test_import_klondike():
+ import klondike
+
+
+@pytest.mark.level(10)
+def test_import_vytvor_balicek():
+ from klondike import vytvor_balicek
+
+
+@pytest.mark.level(11)
+def test_vytvor_balicek_52():
+ """Balíček by měl obsahovat 52 karet"""
+ from klondike import vytvor_balicek
+ assert len(vytvor_balicek()) == 52
+
+
+@pytest.mark.level(11)
+def test_vytvor_balicek_bez_duplikatu():
+ """Balíček by neměl obsahovat duplikáty"""
+ from klondike import vytvor_balicek
+ balicek = vytvor_balicek()
+ for karta in balicek:
+ assert balicek.count(karta) == 1
+
+
+@pytest.mark.level(12)
+@pytest.mark.parametrize('hodnota', range(2, 14))
+def test_vytvor_balicek_pocet_hodnoty(hodnota):
+ """Balíček by měl obsahovat 4 karty dané hodnoty"""
+ from klondike import vytvor_balicek
+ balicek = vytvor_balicek()
+ pocet = 0
+ for hodnota_karty, barva_karty, je_licem_nahoru in balicek:
+ if hodnota_karty == hodnota:
+ pocet = pocet + 1
+ assert pocet == 4
+
+
+@pytest.mark.level(12)
+@pytest.mark.parametrize('barva', ['Pi', 'Sr', 'Ka', 'Kr'])
+def test_vytvor_balicek_pocet_barvy(barva):
+ """Balíček by měl obsahovat 13 karet dané barvy"""
+ from klondike import vytvor_balicek
+ balicek = vytvor_balicek()
+ pocet = 0
+ for hodnota_karty, barva_karty, je_licem_nahoru in balicek:
+ if barva_karty == barva:
+ pocet = pocet + 1
+ assert pocet == 13
+
+
+@pytest.mark.level(13)
+def test_zamichani_balicku():
+ """Každá hra by měla být jiná"""
+ from klondike import vytvor_balicek
+ balicek1 = vytvor_balicek()
+ balicek2 = vytvor_balicek()
+
+ # Je šance 1 z 80658175170943878571660636856403766975289505440883277824000000000000,
+ # že dva náhodné balíčky budou stejné.
+ # Nejspíš je pravděpodobnější, že v průběhu testu odejde počítač,
+ # na kterém test běží, než aby se ty karty zamíchaly stejně.
+ assert balicek1 != balicek2, 'Karty nejsou zamíchané!'
+
+@pytest.mark.level(20)
+def test_import_popis_popis_balicek():
+ from klondike import popis_balicek
+
+
+@pytest.mark.level(21)
+def test_popis_balicek():
+ from klondike import popis_balicek
+ karty = [
+ (13, 'Pi', True),
+ (12, 'Sr', True),
+ (11, 'Ka', True),
+ (10, 'Kr', False)
+ ]
+ assert popis_balicek(karty) == '[K♠ ] [Q ♥] [J ♦] [???]'
+
+
+@pytest.mark.level(22)
+def test_popis_prazdny_balicek():
+ from klondike import popis_balicek
+ assert popis_balicek([]) == ''
+
+
+@pytest.mark.level(30)
+def test_import_popis_vrchni_kartu():
+ from klondike import popis_vrchni_kartu
+
+
+@pytest.mark.level(31)
+def test_popis_vrchni_kartu_jedna_karta():
+ """Balíček se srdcovou dámou by se měl popsat jako tato karta"""
+ from klondike import popis_vrchni_kartu
+ karta = 12, 'Sr', True
+ assert popis_vrchni_kartu([karta]) == '[Q ♥]'
+
+
+@pytest.mark.level(31)
+def test_popis_vrchni_kartu_moc_karet():
+ """Balíček se víc kartama by se měl popsat jako vrchní karta"""
+ from klondike import popis_vrchni_kartu
+ rubem_nahoru = 1, 'Sr', False
+ karta = 12, 'Sr', True
+ balicek = [rubem_nahoru, rubem_nahoru, rubem_nahoru, karta]
+ assert popis_vrchni_kartu(balicek) == '[Q ♥]'
+
+
+@pytest.mark.level(31)
+def test_popis_vrchni_kartu_rubem_nahoru():
+ """Balíček s vrchní kartou rubem nahoru by se měl popsat jako [???]"""
+ from klondike import popis_vrchni_kartu
+ rubem_nahoru = 1, 'Sr', False
+ karta = 12, 'Sr', True
+ balicek = [karta, karta, karta, rubem_nahoru]
+ assert popis_vrchni_kartu(balicek) == '[???]'
+
+@pytest.mark.level(32)
+def test_popis_vrchni_kartu_prazdneho_balicku():
+ """Prázdný balíček se popisuje pomocí [ ]"""
+ from klondike import popis_vrchni_kartu
+ assert popis_vrchni_kartu([]) == '[ ]'
+
+
+@pytest.mark.level(40)
+def test_import_rozdej_sloupecky():
+ from klondike import rozdej_sloupecky
+
+
+@pytest.mark.level(41)
+def test_rozdej_sloupecky_7():
+ """Rozdaných sloupečků má být 7"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+ assert len(sloupecky) == 7
+
+
+@pytest.mark.level(41)
+def test_rozdej_sloupecky_velikost_balicku():
+ """V balíčku by měly chybět karty ze sloupečků"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+
+ # Ceklový počet karet ve sloupečcích
+ velikost_vsech_sloupecku = 0
+ for sloupecek in sloupecky:
+ velikost_vsech_sloupecku = velikost_vsech_sloupecku + len(sloupecek)
+
+ # Kontrola počtu karet v balíčku
+ assert len(balicek) == 52 - velikost_vsech_sloupecku
+
+
+@pytest.mark.level(41)
+def test_rozdej_sloupecky_zvrchu_balicku():
+ """Karty by měly být rozdané z konce balíčku"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ kopie_puvodniho_balicku = list(balicek)
+ sloupecky = rozdej_sloupecky(balicek)
+
+ assert balicek == kopie_puvodniho_balicku[:len(balicek)]
+
+
+@pytest.mark.level(41)
+def test_rozdej_sloupecky_nechybi_karty():
+ """V balíčku a sloupečcích by měly být všechny karty"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ from karty import otoc_kartu
+ balicek = vytvor_balicek()
+ kopie_puvodniho_balicku = list(balicek)
+ sloupecky = rozdej_sloupecky(balicek)
+
+ vsechny_karty = list(balicek)
+ for sloupecek in sloupecky:
+ for karta in sloupecek:
+ vsechny_karty.append(otoc_kartu(karta, False))
+
+ vsechny_karty.sort()
+ kopie_puvodniho_balicku.sort()
+
+ assert vsechny_karty == kopie_puvodniho_balicku
+
+
+@pytest.mark.level(41)
+def test_rozdej_sloupecky_balicek_rubem_nahoru():
+ """Po rozdání sloupečků by měl celý balíček být rubem nahoru"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+
+ for hodnota, barva, je_licem_nahoru in balicek:
+ assert not je_licem_nahoru
+
+
+@pytest.mark.level(42)
+@pytest.mark.parametrize('cislo_sloupce', range(7))
+def test_rozdej_sloupecky_posledni_licem_nahoru(cislo_sloupce):
+ """Poslední karta sloupečku je lícem nahoru"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+ posledni_karta = sloupecky[cislo_sloupce][-1]
+ hodnota, barva, je_licem_nahoru = posledni_karta
+ assert je_licem_nahoru
+
+
+@pytest.mark.level(42)
+@pytest.mark.parametrize('cislo_sloupce', range(7))
+def test_rozdej_sloupecky_ostatni_rubem_nahoru(cislo_sloupce):
+ """Karty pod první kartou sloupečku jsou rubem nahoru"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+ for karta in sloupecky[cislo_sloupce][:-1]:
+ hodnota, barva, je_licem_nahoru = karta
+ assert not je_licem_nahoru
+
+
+@pytest.mark.level(43)
+@pytest.mark.parametrize('cislo_sloupce', range(7))
+def test_rozdej_sloupecky_velikost(cislo_sloupce):
+ """Kontrola velikosti rozdaného sloupečku"""
+ from klondike import vytvor_balicek, rozdej_sloupecky
+ balicek = vytvor_balicek()
+ sloupecky = rozdej_sloupecky(balicek)
+ assert len(sloupecky[cislo_sloupce]) == cislo_sloupce + 1
+
+
+@pytest.mark.level(50)
+def test_import_vypis_sloupecky():
+ from klondike import vypis_sloupecky
+
+
+def check_text(got, expected):
+ got = re.sub(' +\n', '\n', got) # odstraní mezery z konců řádků
+ print(got)
+ assert got.strip() == textwrap.dedent(expected).strip()
+
+
+@pytest.mark.level(51)
+def test_vypis_prazdne_sloupecky(capsys):
+ from klondike import vypis_sloupecky
+ vypis_sloupecky([[], [], [], [], [], [], [], []])
+ out, err = capsys.readouterr()
+ check_text(out, "")
+
+
+@pytest.mark.level(51)
+def test_vypis_sloupecky_jedna_karta_rubem_nahoru(capsys):
+ from klondike import vypis_sloupecky
+ vypis_sloupecky([[(1, 'Pi', False)]] * 7)
+ out, err = capsys.readouterr()
+ check_text(out, "[???] [???] [???] [???] [???] [???] [???]")
+
+
+@pytest.mark.level(51)
+def test_vypis_sloupecky_po_jedne_karte_licem_nahoru(capsys):
+ from klondike import vypis_sloupecky
+ vypis_sloupecky([
+ [(1, 'Pi', True)],
+ [(2, 'Sr', True)],
+ [(3, 'Ka', True)],
+ [(4, 'Kr', True)],
+ [(5, 'Pi', True)],
+ [(6, 'Sr', True)],
+ [(7, 'Ka', True)],
+ ])
+ out, err = capsys.readouterr()
+ check_text(out, "[A♠ ] [2 ♥] [3 ♦] [4♣ ] [5♠ ] [6 ♥] [7 ♦]")
+
+
+@pytest.mark.level(52)
+def test_vypis_sloupecky_dvou_kartach(capsys):
+ from klondike import vypis_sloupecky
+ vypis_sloupecky([
+ [(1, 'Pi', True), (7, 'Sr', True)],
+ [(2, 'Sr', True), (6, 'Ka', True)],
+ [(3, 'Ka', True), (5, 'Kr', False)],
+ [(4, 'Kr', False), (4, 'Pi', True)],
+ [(5, 'Pi', False), (3, 'Sr', True)],
+ [(6, 'Sr', True), (2, 'Ka', True)],
+ [(7, 'Ka', True), (1, 'Kr', True)],
+ ])
+ out, err = capsys.readouterr()
+ check_text(out, """
+ [A♠ ] [2 ♥] [3 ♦] [???] [???] [6 ♥] [7 ♦]
+ [7 ♥] [6 ♦] [???] [4♠ ] [3 ♥] [2 ♦] [A♣ ]
+ """)
+
+
+@pytest.mark.level(52)
+def test_vypis_sloupecky_vice_karet(capsys):
+ from klondike import vypis_sloupecky
+ vypis_sloupecky([
+ [(1, 'Pi', True)],
+ [(2, 'Pi', True), (2, 'Sr', True)],
+ [(3, 'Pi', True), (3, 'Sr', True), (3, 'Ka', True)],
+ [(4, 'Pi', True), (4, 'Sr', True), (4, 'Ka', False), (4, 'Kr', True)],
+ [(5, 'Pi', True), (5, 'Sr', True), (5, 'Ka', True)],
+ [(6, 'Pi', True), (6, 'Sr', True)],
+ [(7, 'Pi', True)],
+ ])
+ out, err = capsys.readouterr()
+ check_text(out, """
+ [A♠ ] [2♠ ] [3♠ ] [4♠ ] [5♠ ] [6♠ ] [7♠ ]
+ [2 ♥] [3 ♥] [4 ♥] [5 ♥] [6 ♥]
+ [3 ♦] [???] [5 ♦]
+ [4♣ ]
+ """)
+
+
+@pytest.mark.level(52)
+def test_vypis_sloupecky_ruby(capsys):
+ """Kontrola výpisu sloupečků, kde jsou všechny karty rubem nahoru"""
+ from klondike import vypis_sloupecky
+ sloupecky = [
+ [(13, 'Pi', False)] * 2,
+ [(13, 'Pi', False)] * 3,
+ [(13, 'Pi', False)] * 4,
+ [(13, 'Pi', False)] * 5,
+ [(13, 'Pi', False)] * 6,
+ [(13, 'Pi', False)] * 7,
+ [(13, 'Pi', False)] * 8,
+ ]
+ vypis_sloupecky(sloupecky)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ [???] [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???]
+ [???] [???] [???] [???]
+ [???] [???] [???]
+ [???] [???]
+ [???]
+ """)
+
+
+@pytest.mark.level(52)
+def test_vypis_sloupecky_zacatek_hry(capsys):
+ """Kontrola výpisu sloupečků, kde jsou karty i rubem lícem nahoru"""
+ from klondike import vypis_sloupecky
+ sloupecky = [
+ [(13, 'Pi', False)] * 0 + [(8, 'Kr', True)],
+ [(13, 'Pi', False)] * 1 + [(9, 'Ka', True)],
+ [(13, 'Pi', False)] * 2 + [(10, 'Sr', True)],
+ [(13, 'Pi', False)] * 3 + [(1, 'Ka', True)],
+ [(13, 'Pi', False)] * 4 + [(4, 'Pi', True)],
+ [(13, 'Pi', False)] * 5 + [(9, 'Kr', True)],
+ [(13, 'Pi', False)] * 6 + [(12, 'Sr', True)],
+ ]
+ vypis_sloupecky(sloupecky)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ [8♣ ] [???] [???] [???] [???] [???] [???]
+ [9 ♦] [???] [???] [???] [???] [???]
+ [X ♥] [???] [???] [???] [???]
+ [A ♦] [???] [???] [???]
+ [4♠ ] [???] [???]
+ [9♣ ] [???]
+ [Q ♥]
+ """)
+
+
+@pytest.mark.level(52)
+def test_vypis_sloupecky_rozehrana(capsys):
+ """Kontrola výpisu sloupečků rozehrané hry"""
+ from klondike import vypis_sloupecky
+ sloupecky = [
+ [(13, 'Pi', False)] * 1 + [(8, 'Kr', True)],
+ [(13, 'Pi', False)] * 8 + [(9, 'Ka', True)],
+ [(13, 'Pi', False)] * 2 + [(10, 'Sr', True), (9, 'Kr', True), (8, 'Ka', True)],
+ [(13, 'Pi', False)] * 6 + [(3, 'Ka', True)],
+ [(13, 'Pi', False)] * 1 + [(4, 'Pi', True)],
+ [(13, 'Pi', False)] * 9 + [(9, 'Kr', True)],
+ [(13, 'Pi', False)] * 5 + [(12, 'Sr', True), (11, 'Pi', True)],
+ ]
+ vypis_sloupecky(sloupecky)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ [???] [???] [???] [???] [???] [???] [???]
+ [8♣ ] [???] [???] [???] [4♠ ] [???] [???]
+ [???] [X ♥] [???] [???] [???]
+ [???] [9♣ ] [???] [???] [???]
+ [???] [8 ♦] [???] [???] [???]
+ [???] [???] [???] [Q ♥]
+ [???] [3 ♦] [???] [J♠ ]
+ [???] [???]
+ [9 ♦] [???]
+ [9♣ ]
+ """)
+
+
+@pytest.mark.level(60)
+def test_import_presun_kartu():
+ from klondike import presun_kartu
+
+
+@pytest.mark.level(61)
+def test_presun_kartu_licem_nahoru():
+ """Kontrola přesunutí karty, co je na začátku lícem nahoru"""
+ from klondike import presun_kartu
+ zdroj = [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+ cil = [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ ]
+ presun_kartu(zdroj, cil, True)
+ assert zdroj == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ (5, 'Kr', True),
+ ]
+ presun_kartu(zdroj, cil, False)
+ assert zdroj == [
+ (3, 'Kr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ (5, 'Kr', True),
+ (4, 'Sr', False),
+ ]
+
+
+@pytest.mark.level(61)
+def test_presun_kartu_rubem_nahoru():
+ """Kontrola přesunutí karty, co je na začátku rubem nahoru"""
+ from klondike import presun_kartu
+ zdroj = [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ ]
+ cil = [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+ presun_kartu(zdroj, cil, True)
+ assert zdroj == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ ]
+ assert cil == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ (13, 'Pi', True),
+ ]
+ presun_kartu(zdroj, cil, False)
+ assert zdroj == [
+ (11, 'Pi', True),
+ ]
+ assert cil == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ (13, 'Pi', True),
+ (12, 'Ka', False),
+ ]
+
+
+@pytest.mark.level(70)
+def test_import_presun_nekolik_karet():
+ from klondike import presun_nekolik_karet
+
+
+@pytest.mark.level(71)
+def test_presun_jednu_kartu():
+ """Zkontroluje přesunutí jedné karty pomocí presun_nekolik_karet"""
+ from klondike import presun_nekolik_karet
+ zdroj = [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+ cil = [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ ]
+ presun_nekolik_karet(zdroj, cil, 1)
+ assert zdroj == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+
+
+@pytest.mark.level(71)
+def test_presun_dve_karty():
+ """Zkontroluje přesunutí dvou karet najednou"""
+ from klondike import presun_nekolik_karet
+ zdroj = [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+ cil = [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ ]
+ presun_nekolik_karet(zdroj, cil, 2)
+ assert zdroj == [
+ (3, 'Kr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+
+
+@pytest.mark.level(71)
+def test_presun_tam_a_zpet():
+ """Zkontroluje přesouvání karet tam a zpátky"""
+ from klondike import presun_nekolik_karet
+ zdroj = [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (5, 'Kr', False),
+ ]
+ cil = [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ ]
+ presun_nekolik_karet(zdroj, cil, 1)
+ assert zdroj == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+ presun_nekolik_karet(cil, zdroj, 2)
+ assert zdroj == [
+ (3, 'Kr', False),
+ (4, 'Sr', False),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ ]
+ presun_nekolik_karet(zdroj, cil, 3)
+ assert zdroj == [
+ (3, 'Kr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (12, 'Ka', True),
+ (4, 'Sr', False),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+ presun_nekolik_karet(cil, zdroj, 4)
+ assert zdroj == [
+ (3, 'Kr', False),
+ (12, 'Ka', True),
+ (4, 'Sr', False),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ ]
+ presun_nekolik_karet(zdroj, cil, 5)
+ assert zdroj == [
+ ]
+ assert cil == [
+ (11, 'Pi', True),
+ (3, 'Kr', False),
+ (12, 'Ka', True),
+ (4, 'Sr', False),
+ (13, 'Pi', True),
+ (5, 'Kr', False),
+ ]
+
+
+@pytest.mark.level(80)
+def test_import_udelej_hru():
+ from klondike import udelej_hru
+
+@pytest.mark.level(81)
+def test_udelej_hru_klice():
+ """Hra by měl být slovník s klíči A až G a U až Z."""
+ from klondike import udelej_hru
+ hra = udelej_hru()
+ assert sorted(hra) == list('ABCDEFGUVWXYZ')
+
+
+@pytest.mark.level(81)
+@pytest.mark.parametrize('pismenko', 'ABCDEFGUVWXYZ')
+def test_pocty_karet(pismenko):
+ """Počty karet v jednotlivých sloupcích jsou dané."""
+ from klondike import udelej_hru
+ hra = udelej_hru()
+
+ POCTY = {
+ 'U': 24,
+ 'V': 0, 'W': 0, 'X': 0, 'Y': 0, 'Z': 0,
+ 'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7,
+ }
+ pozadovany_pocet = POCTY[pismenko]
+
+ assert len(hra[pismenko]) == pozadovany_pocet
+
+
+@pytest.mark.level(81)
+def test_otoceni_karet_balicku():
+ """Karty balíčku by měly být rubem nahoru"""
+ from klondike import udelej_hru
+ hra = udelej_hru()
+ for hodnota, barva, licem_nahoru in hra['U']:
+ assert not licem_nahoru
+
+
+@pytest.mark.level(81)
+@pytest.mark.parametrize('pismenko', 'ABCDEFG')
+def test_otoceni_karet_sloupecku(pismenko):
+ """Karty sloupečků by měly být rubem nahoru, kromě té poslední"""
+ from klondike import udelej_hru
+ hra = udelej_hru()
+ sloupecek = hra[pismenko]
+
+ # Poslední karta
+ posledni_karta = sloupecek[-1]
+ hodnota, barva, licem_nahoru = posledni_karta
+ assert licem_nahoru
+
+ # Ostatní karty
+ for hodnota, barva, licem_nahoru in sloupecek[:-1]:
+ assert not licem_nahoru
+
+
+@pytest.mark.level(81)
+def test_zamichani_hry():
+ """Každá hra by měla být jiná"""
+ from klondike import udelej_hru
+ hra1 = udelej_hru()
+ hra2 = udelej_hru()
+
+ # Je šance 1 z 80658175170943878571660636856403766975289505440883277824000000000000,
+ # že dvě náhodné hry budou stejné.
+ # Nejspíš je pravděpodobnější, že v průběhu testu odejde počítač,
+ # na kterém test běží, než aby se ty karty zamíchaly stejně.
+ assert hra1 != hra2, 'Karty nejsou zamíchané!'
+
+
+@pytest.mark.level(81)
+def test_vsech_karet():
+ """Hra by měla obsahovat všech 52 karet, bez duplikátů."""
+ from klondike import udelej_hru
+ hra = udelej_hru()
+
+ # Uděláme seznam dvojic (hodnota, barva), tedy karet s ignorovaným otočením
+ dvojice_z_hry = []
+ for balicek in hra.values():
+ for hodnota, barva, licem_nahoru in balicek:
+ dvojice_z_hry.append((hodnota, barva))
+ # Seznam seřadíme -- na pořadí nezáleží
+ dvojice_z_hry.sort()
+
+ # Uděláme seznam dvojic (hodnota, barva) všech karet, kteŕe ve hře mají být
+ pozadovane_dvojice = []
+ for hodnota in range(1, 14):
+ for barva in 'Ka', 'Kr', 'Pi', 'Sr':
+ pozadovane_dvojice.append((hodnota, barva))
+ # Tenhle seznam by měl být už seřazený, ale opatrnosti není nikdy dost
+ pozadovane_dvojice.sort()
+
+ # Ty dva seznamy (ten ze hry a ten z testu) by měly být stejné
+ assert dvojice_z_hry == pozadovane_dvojice
+
+
+@pytest.mark.level(90)
+def test_import_vypis_hru():
+ from klondike import vypis_hru
+
+@pytest.mark.level(91)
+def test_ruby(capsys):
+ """Kontrola výpisu hry, kde jsou všechny karty rubem nahoru"""
+ from klondike import udelej_hru, vypis_hru
+ hra = udelej_hru()
+ hra = {
+ 'U': [(13, 'Pi', False)],
+ 'V': [],
+ 'W': [],
+ 'X': [],
+ 'Y': [],
+ 'Z': [],
+ 'A': [(13, 'Pi', False)] * 2,
+ 'B': [(13, 'Pi', False)] * 3,
+ 'C': [(13, 'Pi', False)] * 4,
+ 'D': [(13, 'Pi', False)] * 5,
+ 'E': [(13, 'Pi', False)] * 6,
+ 'F': [(13, 'Pi', False)] * 7,
+ 'G': [(13, 'Pi', False)] * 8,
+ }
+ vypis_hru(hra)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ U V W X Y Z
+ [???] [ ] [ ] [ ] [ ] [ ]
+
+ A B C D E F G
+ [???] [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???] [???]
+ [???] [???] [???] [???] [???]
+ [???] [???] [???] [???]
+ [???] [???] [???]
+ [???] [???]
+ [???]
+ """)
+
+
+@pytest.mark.level(91)
+def test_zacatek_hry(capsys):
+ """Kontrola výpisu hry, kde jsou karty i rubem lícem nahoru"""
+ from klondike import vypis_hru
+ hra = {
+ 'U': [(13, 'Pi', False)],
+ 'V': [(8, 'Kr', True), (13, 'Pi', True)],
+ 'W': [],
+ 'X': [],
+ 'Y': [],
+ 'Z': [],
+ 'A': [(13, 'Pi', False)] * 0 + [(8, 'Kr', True)],
+ 'B': [(13, 'Pi', False)] * 1 + [(9, 'Ka', True)],
+ 'C': [(13, 'Pi', False)] * 2 + [(10, 'Sr', True)],
+ 'D': [(13, 'Pi', False)] * 3 + [(1, 'Ka', True)],
+ 'E': [(13, 'Pi', False)] * 4 + [(4, 'Pi', True)],
+ 'F': [(13, 'Pi', False)] * 5 + [(9, 'Kr', True)],
+ 'G': [(13, 'Pi', False)] * 6 + [(12, 'Sr', True)],
+ }
+ vypis_hru(hra)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ U V W X Y Z
+ [???] [K♠ ] [ ] [ ] [ ] [ ]
+
+ A B C D E F G
+ [8♣ ] [???] [???] [???] [???] [???] [???]
+ [9 ♦] [???] [???] [???] [???] [???]
+ [X ♥] [???] [???] [???] [???]
+ [A ♦] [???] [???] [???]
+ [4♠ ] [???] [???]
+ [9♣ ] [???]
+ [Q ♥]
+ """)
+
+
+@pytest.mark.level(91)
+def test_rozehrana(capsys):
+ from klondike import vypis_hru
+ """Kontrola výpisu rozehrané hry"""
+ hra = {
+ 'U': [(13, 'Pi', False)],
+ 'V': [(8, 'Kr', True), (13, 'Pi', True)],
+ 'W': [(1, 'Pi', True)],
+ 'X': [(1, 'Kr', True)],
+ 'Y': [(1, 'Sr', True)],
+ 'Z': [(1, 'Ka', True), (2, 'Ka', True)],
+ 'A': [(13, 'Pi', False)] * 1 + [(8, 'Kr', True)],
+ 'B': [(13, 'Pi', False)] * 8 + [(9, 'Ka', True)],
+ 'C': [(13, 'Pi', False)] * 2 + [(10, 'Sr', True), (9, 'Kr', True), (8, 'Ka', True)],
+ 'D': [(13, 'Pi', False)] * 6 + [(3, 'Ka', True)],
+ 'E': [(13, 'Pi', False)] * 1 + [(4, 'Pi', True)],
+ 'F': [(13, 'Pi', False)] * 9 + [(9, 'Kr', True)],
+ 'G': [(13, 'Pi', False)] * 5 + [(12, 'Sr', True), (11, 'Pi', True)],
+ }
+ vypis_hru(hra)
+ out, err = capsys.readouterr()
+ check_text(out, """
+ U V W X Y Z
+ [???] [K♠ ] [A♠ ] [A♣ ] [A ♥] [2 ♦]
+
+ A B C D E F G
+ [???] [???] [???] [???] [???] [???] [???]
+ [8♣ ] [???] [???] [???] [4♠ ] [???] [???]
+ [???] [X ♥] [???] [???] [???]
+ [???] [9♣ ] [???] [???] [???]
+ [???] [8 ♦] [???] [???] [???]
+ [???] [???] [???] [Q ♥]
+ [???] [3 ♦] [???] [J♠ ]
+ [???] [???]
+ [9 ♦] [???]
+ [9♣ ]
+ """)
diff --git a/lessons/klondike/decks/index.md b/lessons/klondike/decks/index.md
new file mode 100644
index 00000000..7d5d8683
--- /dev/null
+++ b/lessons/klondike/decks/index.md
@@ -0,0 +1,223 @@
+# Klondike Solitaire: Balíčky
+
+Postupně tvoříme hru *Klondike Solitaire*, která bude nakonec fungovat takto:
+
+* Karty se určitým způsobem *rozdají* do několika balíčků, hromádek nebo
+ jiných skupin
+* Dokud hráč *nevyhrál*:
+ * Hráč *udělá tah*: podle určitých pravidel přesune karty z jedné hromádky
+ na druhou
+
+Pro počítačovou verzi to bude potřeba doplnit o zobrazení stavu hry
+a o načítání hráčova tahu:
+
+* Rozdej karty
+* Dokud hráč nevyhrál:
+ * Zobraz stav hry
+ * Zeptej se hráče, kam chce hrát
+ * Je-li to možné:
+ * Proveď tah
+ * Jinak:
+ * Vynadej hráči, že daný tah nedává smysl
+* Pogratuluj hráči
+
+(Hráč může i prohrát, ale na to může přijít sám a hru ukončit.)
+
+Minule jsme počítač naučil{{gnd('i', 'y', both='i')}} co to je *karta*
+a jak vytvořit zamíchaný *balíček*.
+Pojďme se konečně vrhnout na první krok výše: rozdávání.
+
+
+## Rozdání sloupečků
+
+Karty se určitým způsobem *rozdají* do několika balíčků, hromádek nebo
+jiných skupin.
+Pro přehlednost si tyto skupiny označíme písmenky:
+
+* Dobírací balíčky `U`, `V`, ze kterých se berou karty.
+* Cílové hromádky `W`-`Z`, kam se dávají seřazené karty. Cíl hry je do těchto
+ hromádek dát všechny karty.
+* 7 sloupečků `A`-`G`, kde hráč může s kartami manipulovat.
+
+Prvotní rozdání karet spočívá v tom, že rozdáš karty do 7 sloupečků.
+Nerozdané karty zůstanou v balíčku `U`; ostatní místa na karty budou prázdná:
+
+{{ figure(img=static('game.png'), alt="Ukázka sloupečků") }}
+
+```plain
+ U V W X Y Z
+ [???] [ ] [ ] [ ] [ ] [ ]
+
+ A B C D E F G
+ [3♣ ] [???] [???] [???] [???] [???] [???]
+ [5 ♥] [???] [???] [???] [???] [???]
+ [6♣ ] [???] [???] [???] [???]
+ [5♠ ] [???] [???] [???]
+ [Q ♥] [???] [???]
+ [4♠ ] [???]
+ [3 ♦]
+```
+
+V N-tém sloupečku (počítáno od nuly) je N
+karet rubem nahoru plus jedna karta lícem nahoru.
+Karty do sloupečků se z balíčku rozdávají postupně: vždy se lízne
+vrchní (poslední) karta z balíčku a dá se na konec sloupečku.
+
+
+Napiš následující funkci:
+
+```python
+def rozdej_sloupecky(balicek):
+ """Rozdá z daného balíčku 7 "sloupečků" -- seznamů karet
+
+ Karty ve sloupečcích jsou odstraněny z balíčku.
+ Vrátí všechny sloupečky -- tedy seznam (nebo n-tici) sedmi seznamů.
+ """
+```
+
+Například:
+
+```pycon
+>>> balicek = priprav_balicek()
+>>> sloupecky = rozdej_sloupecky(balicek)
+>>> popis_seznam_karet(sloupecky[0])
+[3♣ ]
+>>> popis_seznam_karet(sloupecky[1])
+[???] [5 ♥]
+>>> popis_seznam_karet(sloupecky[2])
+[???] [???] [6♣ ]
+>>> popis_seznam_karet(sloupecky[6])
+[???] [???] [???] [???] [???] [???] [3 ♦]
+>>> len(balicek) # Z balíčku zmizely karty, které jsou ve sloupečcích
+24
+```
+
+Jak tahle funkce funguje?
+
+* Vytvoří prázdný seznam sloupečků
+* Sedmkrat (pro N od 0 do 6):
+ * Vytvoří prázdný sloupeček (seznam)
+ * N-krát za sebou:
+ * „Lízne“ (`pop`) kartu zvrchu balíčku
+ * Dá líznutou kartu na vršek sloupečku (`append`)
+ * „Lízne“ (`pop`) kartu zvrchu balíčku
+ * Líznutou kartu otočí lícem nahoru (`otoc_kartu`)
+ a dá vršek sloupečku (`append`)
+ * Hotový sloupeček přidá do seznamu sloupečků
+* Výsledné sloupečky vrátí
+
+Pro ověření spusť testy:
+
+* level 40: Funkce existuje
+* level 41: Funkce vrací seznam sedmi seznamů
+* level 42:
+ * V každém sloupečku je aspoň jedna karta
+ * Poslední karta je lícem nahoru
+* level 43: V každém sloupečku je správný počet karet rubem nahoru
+
+
+## Vypsání sloupečků
+
+Vzpomínáš si na základní schéma hry?
+
+* Rozdej karty
+* Dokud hráč nevyhrál:
+ * Zobraz stav hry
+ * Zeptej se hráče, kam chce hrát
+ * Je-li to možné:
+ * Proveď tah
+ * Jinak:
+ * Vynadej hráči, že daný tah nedává smysl
+* Pogratuluj hráči
+
+Rozdání balíčku a sloupečků už víceméně máš!
+Pro teď přeskočíme zjišťování, jestli hráč vyhrál, a koukneme se na zobrazení
+stavu hry.
+
+Například, pokud jsou sloupečky tyto:
+
+```python
+sloupecky = [
+ [(1, 'Pi', True), (7, 'Sr', True)],
+ [(2, 'Sr', True), (6, 'Ka', True)],
+ [(3, 'Ka', True), (5, 'Kr', False)],
+ [(4, 'Kr', False), (4, 'Pi', True)],
+ [(5, 'Pi', False), (3, 'Sr', True)],
+ [(6, 'Sr', True), (2, 'Ka', True)],
+ [(7, 'Ka', True), (1, 'Kr', True), (10, 'Ka', True)],
+]
+```
+
+… můžeš je vypsat jednotlivě:
+
+
+```pycon
+>>> for sloupecek in sloupecky:
+>>> print(popis_seznam_karet(sloupecek))
+[A♠ ] [7 ♥]
+[2 ♥] [6 ♦]
+[3 ♦] [???]
+[???] [4♠ ]
+[???] [3 ♥]
+[6 ♥] [2 ♦]
+[7 ♦] [A♣ ] [X ♦]
+```
+
+To ale není to, co chceme vypsat ve hře: tam se karty v jednom sloupečku
+ukazují pod sebou.
+
+Budeš potřebovat na prvním řádku ukázat první karty ze všech sloupečků,
+na druhém řádku druhé karty ze všech sloupečků, na třetím třetí, atd.
+Pro příklad výše by tedy mělo vyjít:
+
+
+```plain
+[A♠ ] [2 ♥] [3 ♦] [???] [???] [6 ♥] [7 ♦]
+[7 ♥] [6 ♦] [???] [4♠ ] [3 ♥] [2 ♦] [A♣ ]
+ [X ♦]
+```
+
+Znáš funkci, která vezme několik seznamů, a dá ti k dispozici napřed první
+prvky těch seznamů, potom druhé, a tak dál?
+Zkus ji použít!
+
+Pozor, bude tu potřeba pořádně se zamyslet.
+
+```python
+def vypis_sloupecky(sloupecky):
+ """Vypíše sloupečky textově.
+
+ Tato funkce je jen pro zobrazení, používá proto přímo funkci print()
+ a nic nevrací.
+ """
+```
+
+* level 50: Funkce existuje
+* level 51: Funkce vypisuje karty ze věch sloupečků
+* level 52: Funkce funguje, když jsou sloupečky nestejně dlouhé. (Na prázdné místo patří 5 mezer.)
+
+
+## Práce se sloupečky
+
+Aby sis v budoucnu ušetřil{{a}} práci, a aby sis procvičila seznamy,
+zkus teď napsat dvě funkce, které přesunují karty mezi balíčky.
+
+Použij na to metody seznamů (`append`, `extend`, `pop`, příkaz `del`)
+a pomocné funkce, které už máš (`otoc_kartu`).
+
+```python
+def presun_kartu(sloupec_odkud, sloupec_kam, pozadovane_otoceni):
+ """Přesune vrchní kartu ze sloupce "odkud" do sloupce "kam".
+ Karta bude otocena lícem nebo rubem nahoru podle "pozadovane_otoceni".
+ """
+
+def presun_nekolik_karet(sloupec_odkud, sloupec_kam, pocet):
+ """Přesune "pocet" vrchních karet ze sloupce "odkud" do sloupce "kam".
+ Karty se přitom neotáčí.
+ """
+```
+
+* level 60: Funkce `presun_kartu` existuje
+* level 61: Funkce `presun_kartu` funguje dle zadání
+* level 70: Funkce `presun_nekolik_karet` existuje
+* level 71: Funkce `presun_nekolik_karet` funguje dle zadání
diff --git a/lessons/klondike/decks/info.yml b/lessons/klondike/decks/info.yml
new file mode 100644
index 00000000..58f5b2c3
--- /dev/null
+++ b/lessons/klondike/decks/info.yml
@@ -0,0 +1,4 @@
+title: "Klondike: Balíčky"
+style: md
+attribution: Pro PyLadies Brno napsal Petr Viktorin, 2014-2019.
+license: cc-by-sa-40
diff --git a/lessons/klondike/decks/static/game.png b/lessons/klondike/decks/static/game.png
new file mode 100644
index 00000000..1d171a3c
Binary files /dev/null and b/lessons/klondike/decks/static/game.png differ
diff --git a/lessons/klondike/decks/static/klondike.png b/lessons/klondike/decks/static/klondike.png
new file mode 100644
index 00000000..61d321e4
Binary files /dev/null and b/lessons/klondike/decks/static/klondike.png differ
diff --git a/lessons/klondike/game/index.md b/lessons/klondike/game/index.md
new file mode 100644
index 00000000..148f96ad
--- /dev/null
+++ b/lessons/klondike/game/index.md
@@ -0,0 +1,279 @@
+# Klondike Solitaire: Hra
+
+Klondike Solitaire zbývá dát konečně dohromady kousky, které jsme
+v několika posledních lekcích připravovali!
+
+V těchto materiálech najdeš hotové funkce, které je dobré si prohlédnout
+a porozumět jim, ale pak si je můžeš zkopírovat do svého kódu.
+Velké procvičení seznamů a slovníků přijde na konci.
+
+## Hra
+
+Vzpomínáš si na schéma hry?
+
+* Rozdej balíček a sloupečky karet
+* Dokud hráč nevyhrál:
+ * Zobraz stav hry
+ * Zeptej se hráče, odkud a kam chce hrát
+ * Je-li to možné:
+ * Proveď tah
+ * Jinak:
+ * Vynadej hráči, že daný tah nedává smysl
+* Pogratuluj hráči
+
+V Pythonu to bude vypadat následovně.
+Program si ulož do modulu `hra.py`:
+
+```python
+from klondike import udelej_hru, vypis_hru, presun_kartu, presun_nekolik_karet
+
+print()
+
+hra = udelej_hru()
+
+while not hrac_vyhral(hra):
+ vypis_hru(hra)
+ odkud, kam = nacti_tah()
+ try:
+ udelej_tah(hra, odkud, kam)
+ except ValueError as e:
+ print('Něco je špatně:', e)
+
+vypis_hru(hra)
+print('Gratuluji!')
+```
+
+K tomu, abys doplnila funkce do této hry, budeš potřebovat namodelovat
+onu `hru`.
+Ta se skládá z několika balíčků/sloupečků, tedy seznamů karet.
+Ve výpisu butou pojmenované A-Z:
+
+{{ figure(img=static('game.png'), alt="Ukázka sloupečků") }}
+
+* `U` je dobírací balíček, ze kterého se doplňuje `V`.
+* `V` je balíček, ze kterého můžeš brát karty
+* `W-Z` jsou cílové hromádky. Cílem hry je na ně přemístit všechny
+ karty.
+* `A-G` jsou sloupečky, kde se karty dají přeskládávat.
+
+Těchto 13 pojmenovaných seznamů reprezentuje celý stav rozehrané hry.
+Hru proto budeme reprezentovat slovníkem, kde klíče budou písmenka
+a hodnoty pak jednotlivé seznamy.
+
+Následující funkce takovou hru vytvoří:
+
+```python
+def udelej_hru():
+ """Vrátí slovník reprezentující novou hru.
+ """
+ balicek = vytvor_balicek()
+
+ hra = {
+ 'U': balicek,
+ }
+
+ # V-Z začínají jako prázdné seznamy
+ for pismenko in 'VWXYZ':
+ hra[pismenko] = []
+
+ # A-G jsou sloupečky
+ for pismenko, sloupec in zip('ABCDEFG', rozdej_sloupecky(balicek)):
+ hra[pismenko] = sloupec
+
+ return hra
+```
+
+Další funkce, `vypis_hru`, hru vypíše do konzole pomocí `print`.
+Výsledek bude něco jako:
+
+```plain
+ U V W X Y Z
+ [???] [ ] [ ] [ ] [ ] [ ]
+
+ A B C D E F G
+ [3♣ ] [???] [???] [???] [???] [???] [???]
+ [5 ♥] [???] [???] [???] [???] [???]
+ [6♣ ] [???] [???] [???] [???]
+ [5♠ ] [???] [???] [???]
+ [Q ♥] [???] [???]
+ [4♠ ] [???]
+ [3 ♦]
+```
+
+V téhle funkci není nic moc objevného a testům záleží na každé mezeře,
+takže si ji určitě zkopíruj:
+
+```python
+def vypis_hru(hra):
+ """Vypíše hru textově.
+
+ Tato funkce je jen pro zobrazení, používá proto přímo funkci print()
+ a nic nevrací.
+ """
+ print()
+ print(' U V W X Y Z')
+ print('{} {} {} {} {} {}'.format(
+ popis_vrchni_kartu(hra['U']),
+ popis_vrchni_kartu(hra['V']),
+ popis_vrchni_kartu(hra['W']),
+ popis_vrchni_kartu(hra['X']),
+ popis_vrchni_kartu(hra['Y']),
+ popis_vrchni_kartu(hra['Z']),
+ ))
+ print()
+ print(' A B C D E F G')
+ vypis_sloupecky([
+ hra['A'], hra['B'], hra['C'], hra['D'], hra['E'], hra['F'], hra['G']
+ ])
+ print()
+```
+
+Pro kontrolu můžeš pustit testy:
+
+* Level 70: Funkce `udelej_hru` existuje
+* Level 71: Funkce `udelej_hru` funguje dle zadání
+* Level 80: Funkce `vypis_hru` existuje
+* Level 81: Funkce `vypis_hru` funguje dle zadání
+
+
+## Načtení tahu
+
+Hra se bude ovládat zadáním dvou jmen balíčku: odkud a kam hráč chce kartu
+přesunout.
+
+Tahle funkce není součást logiky hry. Dej ji do `hra.py`, hned za `import`.
+
+```python
+def nacti_tah():
+ while True:
+ tah = input('Tah? ')
+ try:
+ jmeno_zdroje, jmeno_cile = tah.upper()
+ except ValueError:
+ print('Tah zadávej jako dvě písmenka, např. UV')
+ else:
+ return jmeno_zdroje, jmeno_cile
+```
+
+## Zástupné funkce
+
+K úplné hře nám chybí ještě samotná logika hry: `hrac_vyhral` a `udelej_tah`.
+
+Aby ti hra aspoň trochu fungovala, vytvoř si zástupné funkce,
+které nic nekontrolují a nenechají tě vyhrát.
+Dej ji do `hra.py`, opět hned za `import`:
+
+```python
+def hrac_vyhral(hra):
+ """Vrací True, pokud je hra vyhraná.
+ """
+ return False
+
+def udelej_tah(hra, jmeno_odkud, jmeno_kam):
+ """Udělá tah z jednoho místa na druhé.
+
+ Místa jsou označovány velkými písmeny (např. 'A', 'V' nebo 'X').
+
+ Není-li tah možný, vyhodí ValueError s popisem problému.
+ """
+ presun_kartu(hra[jmeno_odkud], hra[jmeno_kam], True)
+```
+
+Obě bude ještě potřeba upravit, ale teď už si můžeš hru víceméně zahrát!
+Zkus si to!
+
+
+## Jiné rozhraní
+
+Celý tento projekt píšeš ve funkcích s daným jménem a s daným počtem a významem
+argumentů.
+To má dvě výhody.
+
+První z nich je testování: připravené testy importují tvé funkce a zkouší je,
+takže si můžeš být jist{{a}}, že fungují.
+
+Druhá je zajímavější: máš-li logiku hry, funkce `udelej_hru` `udelej_tah`
+a `hrac_vyhral`, napsané podle specifikací, může je použít i jakýkoli jiný
+program – ne jen ten, který jsi napsal{{a}} ty.
+
+Jeden takový si můžeš vyzkoušet:
+
+* Nainstaluj si do virtuálního prostředí knihovnu `pyglet`, která umí ovládat
+ grafická okýnka:
+
+ ```console
+ (venv)$ python -m pip install pyglet
+ ```
+
+* Stáhni si do aktuálního adresáře soubory [ui.py] a [cards.png].
+
+ [ui.py]: {{ static('ui.py') }}
+ [cards.png]: {{ static('cards.png') }}
+
+* Hru spusť pomocí:
+
+ ```console
+ (venv)$ python ui.py
+ ```
+
+Hra považuje chyby `ValueError` za chyby uživatele, tedy tahy proti pravidlům.
+Zobrazí je v terminálu a v titulku okýnka.
+Ostatní chyby by ve správném programu neměly nastat; objeví se jako normální
+chybové hlášky na terminálu.
+
+*Obrázky karet jsou z [Board Game Pack](https://kenney.nl/assets/boardgame-pack)
+studia [kenney.nl](https://kenney.nl).*
+
+
+## Logika hry
+
+Zbývá doplnit „pravidla hry“ do dvou funkcí, `hrac_vyhral` a `udelej_tah`.
+To už bude na tobě.
+
+### hrac_vyhral
+
+Hráč vyhrál, pokud jsou všechny karty na cílových hromádkách `W`-`Z`.
+
+### udelej_tah
+
+Když tah není podle pravidel, funkce `udelej_tah` vyhodí `ValueError`.
+
+Možné tahy:
+* `U`→`V`:
+ * V balíčku `U` musí něco být
+ * Přesouvá se jedna karta; otočí se lícem nahoru
+* `V`→`U`:
+ * V balíčku U nesmí být nic
+ * Přesouvají se všechny karty, seřazené v opačném pořadí;
+ otočí se rubem nahoru (tj. volej dokola
+ `presun_kartu(hra['V'], hra['U'], False)` dokud ve V něco je)
+* Balíček `V` nebo sloupeček `A`-`G` (zdroj) → hromádka `W`-`Z`:
+ * Přesouvá se jedna karta
+ * Je-li cílová hromádka prázdná:
+ * Musí to být eso
+ * Jinak:
+ * Přesouvaná karta musí mít stejnou barvu jako vrchní karta cílové hromádky
+ * Přesouvaná karta musí být o 1 vyšší než vrchní karta cílové hromádky
+ * Je-li zdroj po přesunu neprázdný, jeho vrchní karta se otočí lícem nahoru
+* Balíček `V` → „cílový“ sloupeček `A`-`G`
+ * Přesouvá se jedna karta
+ * Přesouvaná karta musí pasovat\*⁾ na cílový sloupeček
+* „Zdrojový“ sloupeček `A`-`G` → „cílový“ sloupeček `A`-`G`
+ * Přesouvá se několik karet
+ * (zkontroluj všechny možnosti: 1 až počet karet ve zdrojovém sloupečku;
+ vždy je max. jedna správná možnost)
+ * Všechny přesouvané karty musí být otočené lícem nahoru
+ * První z přesouvaných karet musí pasovat\*⁾ na cílový sloupeček
+* Cíl `W`-`Z` → sloupeček `A`-`G` (nepovinné – jen v některých variantách hry)
+ * Přesouvá se jedna karta
+ * Přesouvaná karta musí pasovat\*⁾ na cílový sloupeček
+
+\*⁾ Kdy přesouvaná karta pasuje na sloupeček?
+* Je-li sloupeček prázdný:
+ * Karta musí být král
+* Jinak:
+ * Barva přesouvané karty musí být opačná než barva vrchní karty sloupečku, tedy:
+ * Červená (♥ nebo ♦) jde dát jen na černou (♠ nebo ♣)
+ * Černá (♠ nebo ♣) jde dát jen na červenou (♥ nebo ♦)
+ * A zároveň musí hodnota přesouvané karty být o 1 nižší než hodnota vrchní
+ karty sloupečku.
diff --git a/lessons/klondike/game/info.yml b/lessons/klondike/game/info.yml
new file mode 100644
index 00000000..15599682
--- /dev/null
+++ b/lessons/klondike/game/info.yml
@@ -0,0 +1,4 @@
+title: "Klondike: Hra"
+style: md
+attribution: Pro PyLadies Brno napsal Petr Viktorin, 2014-2019.
+license: cc-by-sa-40
diff --git a/lessons/klondike/game/static/cards.png b/lessons/klondike/game/static/cards.png
new file mode 100644
index 00000000..832778da
Binary files /dev/null and b/lessons/klondike/game/static/cards.png differ
diff --git a/lessons/klondike/game/static/game.png b/lessons/klondike/game/static/game.png
new file mode 100644
index 00000000..1d171a3c
Binary files /dev/null and b/lessons/klondike/game/static/game.png differ
diff --git a/lessons/klondike/game/static/klondike.png b/lessons/klondike/game/static/klondike.png
new file mode 100644
index 00000000..61d321e4
Binary files /dev/null and b/lessons/klondike/game/static/klondike.png differ
diff --git a/lessons/klondike/game/static/ui.py b/lessons/klondike/game/static/ui.py
new file mode 100644
index 00000000..7be6ce53
--- /dev/null
+++ b/lessons/klondike/game/static/ui.py
@@ -0,0 +1,176 @@
+from pathlib import Path
+import traceback
+
+import pyglet
+
+from klondike import udelej_hru, udelej_tah
+
+
+WINDOW_CAPTION = 'Klondike Solitaire'
+
+SUIT_NAMES = 'Kr', 'Ka', 'Pi', 'Sr'
+
+KEYS = {
+ pyglet.window.key.A: 'A',
+ pyglet.window.key.B: 'B',
+ pyglet.window.key.C: 'C',
+ pyglet.window.key.D: 'D',
+ pyglet.window.key.E: 'E',
+ pyglet.window.key.F: 'F',
+ pyglet.window.key.G: 'G',
+ pyglet.window.key.U: 'U',
+ pyglet.window.key.V: 'V',
+ pyglet.window.key.W: 'W',
+ pyglet.window.key.X: 'X',
+ pyglet.window.key.Y: 'Y',
+ pyglet.window.key.Z: 'Z',
+}
+
+
+image = pyglet.image.load('cards.png')
+card_width = image.width // 14
+card_height = image.height // 4
+
+card_pictures = {}
+for suit_number, suit_name in enumerate(SUIT_NAMES):
+ for value in range(1, 14):
+ card_pictures[value, suit_name] = image.get_region(
+ card_width * value, card_height * suit_number,
+ card_width, card_height,
+ )
+
+card_back_picture = image.get_region(0, card_height, card_width, card_height)
+empty_slot_picture = image.get_region(0, 0, card_width, card_height)
+
+label = pyglet.text.Label('x', color=(0, 200, 100, 255),
+ anchor_x='center', anchor_y='center')
+
+window = pyglet.window.Window(resizable=True, caption=WINDOW_CAPTION)
+press_queue = []
+
+
+def get_dimensions():
+ card_width = window.width / (7*6+1) * 5
+ card_height = card_width * 19/14
+ margin_x = card_width / 5
+ margin_y = margin_x * 2
+ offset_y = margin_x
+ return card_width, card_height, margin_x, margin_y, offset_y
+
+
+def draw_card(card, x, y, x_offset=0, y_offset=0, active=False):
+ card_width, card_height, margin_x, margin_y, offset_y = get_dimensions()
+
+ if card == None:
+ pyglet.gl.glColor4f(0.5, 0.5, 0.5, 0.5)
+ picture = empty_slot_picture
+ else:
+ if active:
+ pyglet.gl.glColor4f(0.75, 0.75, 1, 1)
+ else:
+ pyglet.gl.glColor4f(1, 1, 1, 1)
+ value, suit, is_face_up = card
+ if is_face_up:
+ picture = card_pictures[value, suit]
+ else:
+ picture = card_back_picture
+
+ picture.blit(
+ margin_x + (card_width + margin_x) * x + (x_offset * margin_x / 60),
+ window.height - (margin_y + card_height) * (y+1) - offset_y * y_offset,
+ width=card_width, height=card_height)
+
+
+def draw_label(text, x, y, active):
+ card_width, card_height, margin_x, margin_y, offset_y = get_dimensions()
+
+ label.x = x * (card_width + margin_x) + margin_x + card_width / 2
+ label.y = window.height - y * (card_height + margin_y) - margin_y / 2
+ label.text = text
+ if active:
+ label.color = 200, 200, 255, 255
+ else:
+ label.color = 0, 200, 100, 255
+ label.draw()
+
+
+def draw_deck(letter, deck, x, y, x_offset=0, y_offset=0):
+ active = (letter in press_queue)
+ draw_label(letter, x, y, active)
+ draw_card(None, x, y)
+ for i, card in enumerate(deck):
+ draw_card(card, x, y, x_offset*i, y_offset*i, active)
+
+
+@window.event
+def on_draw():
+ # Enable transparency for images
+ pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
+ pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
+
+ # Green background
+ pyglet.gl.glClearColor(0, 0.25, 0.05, 1)
+ window.clear()
+
+ # Get dimensions
+ card_width, card_height, margin_x, margin_y, offset_y = get_dimensions()
+ label.font_size = margin_y / 2
+
+ # Draw all the cards in the various decks
+ for x, letter in enumerate('UV'):
+ draw_deck(letter, game[letter], x, 0, x_offset=1)
+
+ for x, letter in enumerate('WXYZ'):
+ draw_deck(letter, game[letter], x + 3, 0, x_offset=1)
+
+ for x, letter in enumerate('ABCDEFG'):
+ draw_deck(letter, game[letter], x, 1, y_offset=1)
+
+
+@window.event
+def on_key_press(symbol, mod):
+ if symbol in KEYS:
+ press_queue.append(KEYS[symbol])
+ handle_press_queue()
+
+
+@window.event
+def on_mouse_press(x, y, symbol, mod):
+ card_width, card_height, margin_x, margin_y, offset_y = get_dimensions()
+ if y > window.height - card_height - margin_y:
+ deck_names = 'UV WXYZ'
+ else:
+ deck_names = 'ABCDEFG'
+ deck_name = deck_names[int(x - margin_x/2) // int(card_width + margin_x)]
+ if deck_name.strip():
+ press_queue.append(deck_name)
+ handle_press_queue()
+
+
+def handle_press_queue():
+ if press_queue == ['U']:
+ press_queue.append('V')
+ if len(press_queue) >= 2:
+ source = press_queue[0]
+ destination = press_queue[1]
+ press_queue.clear()
+
+ try:
+ result = udelej_tah(game, source, destination)
+ except ValueError as e:
+ # Print *just* the error message
+ msg = f'{source}→{destination}: {e}'
+ window.set_caption(msg)
+ print(msg)
+ except Exception:
+ # Print the error message, but ignore the error (so the
+ # game can continue)
+ traceback.print_exc()
+ else:
+ print(f'{source}→{destination}: {result}')
+ window.set_caption(WINDOW_CAPTION)
+
+
+game = udelej_hru()
+
+pyglet.app.run()