← Přehled všech hodin

Soubory a JSON — aby si hra pamatovala

Cíl hodiny: Naučíš program pamatovat si věci i po vypnutí. Deník, žebříček skóre i celou postavu si uložíš do souboru — a zase načteš zpátky.
Jak stránku používat: vpravo nahoře je tlačítko Prezentace — schová všechno kromě jednoho bloku a přepíná se šipkami. U ukázek a cvičení je kód vlevo editovatelný: uprav ho a klikni na Spustit (nebo Ctrl+Enter). Program se přeloží a spustí přímo v prohlížeči a vpravo se objeví to, co vypsal do konzole. Nic se neinstaluje.
Dnes to poběží jinak než obvykle. Kód, který sahá na soubory, nemá v prohlížeči kam zapisovat — proto u dnešních dílen nenajdeš tlačítko ▶ Spustit. Kód si můžeš pořád číst i kopírovat, ale vyzkoušet ho musíš u sebe ve VS Code (viz úvodní hodina): založ projekt příkazem dotnet new console, vlož kód do Program.cs a spusť dotnet run. Soubory pak najdeš přímo ve složce projektu — a to je právě ta nejlepší část, protože si je otevřeš v poznámkovém bloku a uvidíš, co tvůj program napsal.

1. Vysvětlení a souvislosti

Poznámky pro učiteleJak celou hodinu vést1. Vysvětlení a souvislosti

Tenhle blok je jen pro vás — dětem ho nepromítejte.

Fáze Co se děje Čas
Rozjezd „Co se stane s proměnnou, když program skončí?" 5 min
Text do souboru Bloky 2–4 + ukázky 1–2 20 min
Kde ten soubor je Blok 5, společné hledání na disku 10 min
JSON Bloky 6–7 + ukázky 3–4 25 min
Uložená hra Ukázka 5 10 min
Cvičení Děti pracují samy, vy obcházíte 55 min
Závěr Kdo si otevřel svůj JSON v poznámkovém bloku? 10 min
Technická příprava — přečtěte si dřív než hodinu pustíte

Dnešní dílny nemají tlačítko ▶ Spustit a je to tak správně. Generátor stránek pozná, že kód obsahuje File., Directory. nebo StreamWriter, a živé spouštění pro takový blok sám vypne — v prohlížeči totiž není disk, na který by se dalo zapsat. Blok se vykreslí jen jako kód s poznámkou „Tenhle kód potřebuje soubory, síť nebo vlákna…".

Praktický důsledek pro hodinu: děti musí mít funkční VS Code s .NET SDK z úvodní hodiny. Než začnete, nechte je ověřit, že v terminálu funguje:

dotnet --version
dotnet new console -o csquest-soubory
cd csquest-soubory
dotnet run

Doporučuju založit jeden projekt na celou hodinu a jen přepisovat Program.cs. Ušetříte tím deset minut a spoustu zmatků se složkami.

Pozor na to, kde soubory vznikají. Relativní cesta "denik.txt" se počítá od pracovní složky, což je u dotnet run složka projektu. Děti tam soubor najdou hned vedle Program.cs — nechte je ho otevřít, ten okamžik stojí za to.

Hlavní pedagogický cíl

Celá hodina je o jedné větě: paměť je dočasná, disk je trvalý. Začněte otázkou „Kam se poděla proměnná, když program skončil?" a nechte je hádat. Teprve pak ukažte File.WriteAllText.

Druhý cíl je pochopit, proč nestačí ukládat holý text: jakmile má postava pět vlastností a seznam předmětů, potřebuješ formát. A ten už někdo vymyslel — jmenuje se JSON a používá ho skoro každá aplikace na světě.

Tři věci, které dnes NEVYSVĚTLUJTE
  1. Streamy (StreamWriter, FileStream). File.WriteAllText stačí na všechno, co dnes potřebujeme. Streamy patří k velkým souborům, a ty neřešíme.
  2. Výjimky a try/catch. Místo nich používáme File.Exists. Je to jednodušší a děti to samy navrhnou.
  3. Serializaci vlastních tříd a [JsonPropertyName]. Třídy přijdou v 16. hodině; dnes stačí Dictionary<string, object>.

Jak pracovat se smíšenou úrovní. Cvičení 1 a 2 zvládnou všichni — jsou to jen texty a řádky. Cvičení 3 a 4 jsou JSON a tam se tempo rozejde; slabším nechte předepsaný slovník a ať doplní jen zápis a čtení. Cvičení 5 je projektové a dá se odnést domů.

Blok 1Proč program po skončení všechno zapomene1. Vysvětlení a souvislosti
int zlato = 250;
Console.WriteLine($"Mám {zlato} zlaťáků.");

// Program skončí…
// …a zlato je pryč. Při dalším spuštění začínáme od nuly.

Proměnné žijí v operační paměti. Ta je rychlá, ale úplně zapomnětlivá: jakmile program skončí, systém jí paměť sebere a je po všem.

Když chceš, aby si hra pamatovala postavu i zítra, musíš data dostat na disk — do souboru. A soubor je jenom pojmenovaný kus textu (nebo dat), který na disku zůstane, dokud ho někdo nesmaže.

V C# na to slouží třída File ze jmenného prostoru System.IO. Nemusíš nic doplňovat, v konzolové aplikaci je k dispozici rovnou.

Dvě metody ti pokryjí devadesát procent všeho:

  • File.WriteAllText(cesta, text) — zapíše text do souboru,
  • File.ReadAllText(cesta) — přečte celý soubor jako jeden string.
Blok 2Zápis, přidávání a čtení1. Vysvětlení a souvislosti
File.WriteAllText("denik.txt", "Den 1: Vstoupil jsem do dungeonu.\n");
File.AppendAllText("denik.txt", "Den 2: Potkal jsem skřeta.\n");
File.AppendAllText("denik.txt", "Den 3: Skřet potkal můj meč.\n");

string obsah = File.ReadAllText("denik.txt");
Console.WriteLine(obsah);

string[] radky = File.ReadAllLines("denik.txt");
Console.WriteLine($"Deník má {radky.Length} zápisů.");

Pozor na jeden zásadní rozdíl:

Metoda Co udělá
File.WriteAllText soubor přepíše — co tam bylo, je pryč
File.AppendAllText text přidá na konec
File.ReadAllText vrátí celý soubor jako jeden string
File.ReadAllLines vrátí string[] — jeden řádek = jeden prvek
File.WriteAllLines zapíše pole řetězců, každý na vlastní řádek

WriteAllText je nejčastější zdroj slz: člověk chce přidat zápis do deníku, omylem použije WriteAllText a přijde o všechno předchozí.

Pokud soubor neexistuje, WriteAllText i AppendAllText ho vyrobí. Zápis tedy nikdy nespadne na tom, že soubor chybí — na rozdíl od čtení.

Znak \n na konci znamená odřádkování. Bez něj by se všechny zápisy slily do jednoho dlouhého řádku.

Blok 3Než čteš, zeptej se, jestli soubor existuje1. Vysvětlení a souvislosti
string cesta = "postava.txt";

if (File.Exists(cesta))
{
    Console.WriteLine("Načítám uloženou postavu…");
    Console.WriteLine(File.ReadAllText(cesta));
}
else
{
    Console.WriteLine("Žádná uložená postava. Zakládám novou.");
    File.WriteAllText(cesta, "Arkon;1;100\n");
}

Čtení neexistujícího souboru program shodí. Hláška vypadá takhle:

Unhandled exception. System.IO.FileNotFoundException:
  Could not find file '/…/postava.txt'.

Řešení je jednoduché a přečteš ho nahlas: „Jestli ten soubor existuje, načti ho; jinak založ nový."

string cesta = "postava.txt";
if (File.Exists(cesta)) { /* načti */ }
else { /* založ */ }

Přesně takhle se chová každá hra, kterou jsi kdy hrál: při startu se podívá, jestli má uloženou pozici. Když má, nabídne „Pokračovat". Když nemá, nabídne jen „Nová hra".

Existenci složky se ptáš metodou Directory.Exists, vyrobíš ji pomocí Directory.CreateDirectory — ta se nezlobí, ani když už složka existuje.

Blok 4Kam se ten soubor vlastně uloží1. Vysvětlení a souvislosti
Console.WriteLine("Pracovní složka:");
Console.WriteLine(Directory.GetCurrentDirectory());

string cesta = Path.Combine("data", "postava.json");
Console.WriteLine($"Relativní cesta: {cesta}");

Directory.CreateDirectory("data");
File.WriteAllText(cesta, "zatím prázdno");

Console.WriteLine("Soubory ve složce data:");
foreach (string soubor in Directory.GetFiles("data"))
{
    Console.WriteLine("  " + soubor);
}

Když napíšeš jen "denik.txt", je to relativní cesta. Počítá se od pracovní složky — a to je při dotnet run složka tvého projektu. Soubor tedy najdeš hned vedle Program.cs.

Jistotu máš vždycky: Directory.GetCurrentDirectory() ti řekne, kde se program právě „nachází".

Cesty nikdy neslepuj ručně pomocí + "/" +. Windows používá \, macOS a Linux /. Od toho je Path.Combine, který dosadí správný oddělovač:

string cesta = Path.Combine("data", "ulozene", "hra.json");
Console.WriteLine(cesta);

A Directory.CreateDirectory("data") je bezpečná i při opakovaném volání — pokud složka existuje, prostě neudělá nic.

Blok 5JSON: formát, kterému rozumí skoro každý program1. Vysvětlení a souvislosti
{
  "jmeno": "Arkon",
  "uroven": 3,
  "zivoty": 80,
  "inventar": [
    "Meč",
    "Lektvar",
    "Zlatý klíč"
  ]
}

Holý text stačí na deník, ale ne na postavu. Jakmile máš jméno, úroveň, životy a seznam předmětů, potřebuješ formát, ve kterém se dá poznat, co je co. Tím formátem je JSON.

Pravidla jsou čtyři a naučíš se je za minutu:

  • složené závorky { } = objekt, tedy dvojice klíč: hodnota,
  • hranaté závorky [ ] = seznam,
  • klíč je vždycky v uvozovkách, hodnota jen když je to text,
  • položky odděluje čárka; za poslední čárka nesmí být.

Vypadá to jako C#? Trochu ano — a to je záměr. JSON vymysleli tak, aby ho člověk přečetl a počítač zpracoval. Používá ho tvůj mobil, každá webová aplikace i nastavení Minecraftu.

A hlavně: JSON je pořád jen text. Můžeš ho otevřít v poznámkovém bloku a ručně opravit hrdinovi životy. (Čemuž se říká cheatování.)

Blok 6Z paměti do JSONu: Serialize1. Vysvětlení a souvislosti
using System.Text.Json;

var postava = new Dictionary<string, object>
{
    ["jmeno"] = "Arkon",
    ["uroven"] = 3,
    ["zivoty"] = 80,
    ["inventar"] = new List<string> { "Meč", "Lektvar" }
};

var nastaveni = new JsonSerializerOptions { WriteIndented = true };
string text = JsonSerializer.Serialize(postava, nastaveni);

File.WriteAllText("postava.json", text);
Console.WriteLine(text);

Serializace znamená „převeď data v paměti na text". V C# to dělá JsonSerializer.Serialize.

Postavu si připravíme jako slovník Dictionary<string, object>: klíč je název vlastnosti, hodnota může být číslo, text i celý seznam. Typ object znamená „cokoliv" — a serializátor si sám pozná, o co jde.

new JsonSerializerOptions { WriteIndented = true } zapne hezké odsazování. Bez něj se JSON zapíše do jednoho dlouhého řádku — počítači to nevadí, ale ty si ho pak nepřečteš.

Proč jsou v souboru divné kódy jako `\u010D`

Serializátor ve výchozím nastavení převede háčky a čárky na únikové kódy: "Me\u010D" místo "Meč". Není to chyba — při čtení zpátky vyjde správně „Meč". Kdo chce v souboru vidět rovnou češtinu, přidá Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (a nahoru using System.Text.Encodings.Web;).

Blok 7Z JSONu zpátky do paměti: Deserialize1. Vysvětlení a souvislosti
using System.Text.Json;

string text = File.ReadAllText("postava.json");

var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(text)
           ?? new Dictionary<string, JsonElement>();

string jmeno = data["jmeno"].GetString() ?? "bezejmenný";
int uroven = data["uroven"].GetInt32();

Console.WriteLine($"{jmeno}, úroveň {uroven}");

foreach (JsonElement polozka in data["inventar"].EnumerateArray())
{
    Console.WriteLine("  • " + polozka.GetString());
}

Deserializace je cesta zpátky: z textu na data. Načteme soubor jako string a necháme JsonSerializer udělat zbytek.

Výsledek si bereme jako Dictionary<string, JsonElement>. JsonElement je „ještě nerozbalená hodnota" — a ty jí řekneš, jak ji chceš přečíst:

Metoda Vrátí
.GetString() text (může být null, proto ?? "")
.GetInt32() celé číslo
.GetDouble() desetinné číslo
.GetBoolean() true / false
.EnumerateArray() prvky seznamu, dají se projít foreach

Dvojité otazníky ?? znamenají „a když by tam bylo nic, vezmi tohle". Deserialize totiž může vrátit null, když je soubor prázdný.

Klíč, který v souboru není, program shodí (KeyNotFoundException). Proto se u nepovinných věcí ptej: if (data.ContainsKey("zlato")).

Poznámky pro učiteleTechnické pozadí — jen pro vás1. Vysvětlení a souvislosti

Proč zrovna File.WriteAllText. Je to obal nad StreamWriter, který otevře, zapíše, zavře a uklidí. Pro soubory do jednotek megabajtů je to naprosto správná volba. Streamy mají smysl až tam, kde se soubor nevejde do paměti — což ve výuce nenastane.

Kódování. File.WriteAllText zapisuje UTF-8 bez BOM. Čeština je tedy v pořádku na všech třech systémech. Kdyby dítě otevřelo soubor ve starém poznámkovém bloku na Windows a vidělo paskvil, jde o kódování zobrazení, ne o chybu programu.

Konce řádků. \n funguje všude; Windows si historicky psal \r\n. File.ReadAllLines si s obojím poradí, takže se tím nemusíme zabývat. Kdo chce být korektní, použije Environment.NewLine.

Dictionary<string, object> versus třídy. System.Text.Json serializuje hodnotu typu object podle jejího skutečného typu za běhu, takže int, string i List<string> se uloží správně. Při čtení už ale object nestačí — proto čteme do JsonElement. V 16. hodině, až budou třídy, se z toho stane JsonSerializer.Deserialize<Postava>(text) a atribut [JsonPropertyName("jmeno")] pro případ, kdy se název vlastnosti v C# liší od klíče v souboru.

Proč tu není try/catch. Záměrně. File.Exists pokryje 95 % případů a je čitelnější. Výjimky nechte na pozdější hodinu — dnes by jen zamlžily pointu. Kdo se zeptá, řekněte pravdu: File.Exists neochrání proti tomu, že soubor někdo smaže o milisekundu později; na to jsou výjimky."

Verze uložené hry. Ve cvičení 5 se do JSONu ukládá klíč "verze". Vypadá to jako zbytečnost, ale je to naprosto reálná praxe: jakmile změníte strukturu uložené hry, staré soubory se rozbijí. Kontrola verze je nejlevnější pojistka na světě a děti to většinou znají z modů do her.

Bezpečnostní poznámka do diskuze. Nikdy neukládejte hesla do souboru jako čitelný text. Zeptejte se, proč — dojdou k tomu sami.

2. Ukázky pro projekci

Připomenutí k celé sekci 2

Dílny v téhle hodině nemají tlačítko ▶ Spustit — sahají na soubory a to v prohlížeči nejde. Kód zkopíruj do Program.cs ve svém projektu a spusť dotnet run. Soubory, které vzniknou, si pak otevři v editoru.

Blok 8Ukázka 1 — Deník výpravy2. Ukázky pro projekci
Studenti zkuste změnit
  • Přidej čtvrtý zápis do deníku.
  • Vyměň AppendAllText za WriteAllText a podívej se, co se stane.
  • Vypiš zápisy očíslované (for přes pole řádků).
  • Vypiš jen zápisy, ve kterých je slovo „skřet".
demo/demo1_denik.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

Co si z toho odnést: WriteAllText zakládá, AppendAllText přidává. Až si soubor denik.txt otevřeš v editoru, uvidíš přesně to, co program vypsal.

Blok 9Ukázka 2 — Žebříček skóre po řádcích2. Ukázky pro projekci
Studenti zkuste změnit
  • Přidej dalšího hrdinu a spusť znovu.
  • Seřaď žebříček od nejmenšího skóre.
  • Vypiš jen tři nejlepší.
  • Přidej ke každému řádku sloupec z podle skóre děleného deseti.
demo/demo2_zebricek.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

Co si z toho odnést: textový soubor s oddělovačem ; je nejjednodušší možná „databáze". Split(';') rozřeže řádek, int.Parse udělá z textu číslo.

Blok 10Ukázka 3 — Postava do JSONu2. Ukázky pro projekci
Studenti zkuste změnit
  • Přidej postavě klíč "zlato" s nějakým číslem.
  • Přidej do inventáře další předmět.
  • Zkus WriteIndented = false a porovnej, jak soubor vypadá.
  • Vypiš délku vzniklého textu (text.Length).
demo/demo3_json_zapis.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

Co si z toho odnést: slovník v paměti a JSON v souboru jsou dvě podoby té samé věci. Serialize je převede tam, Deserialize zpátky.

Blok 11Ukázka 4 — Postava zpátky ze souboru2. Ukázky pro projekci
Studenti zkuste změnit
  • Změň v souboru ručně životy a spusť program znovu.
  • Vypiš životy jako pruh z (jeden znak za deset životů).
  • Přidej kontrolu data.ContainsKey("zlato") a vypiš zlato jen když je.
  • Zkus smazat soubor a podívej se, co program udělá.
demo/demo4_json_cteni.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

Co si z toho odnést: File.Exists je pojistka, JsonElement je „zabalená hodnota" a .GetString() / .GetInt32() ji rozbalí na správný typ.

Blok 12Ukázka 5 — Uložená hra s číslem verze2. Ukázky pro projekci
Studenti zkuste změnit
  • Spusť program dvakrát za sebou a sleduj, jak se změní hlášení.
  • Změň v souboru "verze" na 2 a spusť znovu.
  • Přidej do uložené hry klíč "cas_hrani" s počtem minut.
  • Po načtení přidej hrdinovi 50 zlaťáků a ulož to zpátky.
demo/demo5_ulozena_hra.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

Co si z toho odnést: tohle už je opravdová uložená hra. Spusť ji několikrát a uvidíš, jak hrdinovi roste úroveň i zlato — protože se to pamatuje.

Blok 13Když se něco pokazí2. Ukázky pro projekci
Unhandled exception. System.IO.FileNotFoundException:
  Could not find file '/Users/ty/csquest/postava.json'.

Nejčastější chyba hodiny. Program chce číst soubor, který ještě neexistuje. V hlášce je rovnou celá cesta — přečti si ji, řekne ti přesně, kde program soubor hledal. Často zjistíš, že hledal jinde, než sis myslel.

Oprava je vždycky stejná: před čtením se zeptej.

string cesta = "postava.json";
if (File.Exists(cesta)) { /* čti */ }
else { /* založ výchozí */ }

Druhá klasika je KeyNotFoundException při čtení JSONu — v souboru prostě ten klíč není (třeba proto, že jsi ho přejmenoval). Ověř si ho pomocí data.ContainsKey("zlato"), nebo si soubor otevři a podívej se.

Hláška / projev Co to znamená Jak to opravit
FileNotFoundException soubor neexistuje if (File.Exists(cesta)) před čtením
DirectoryNotFoundException chybí složka v cestě Directory.CreateDirectory("data")
KeyNotFoundException klíč v JSONu není data.ContainsKey("klic")
JsonException: ... is invalid poškozený JSON (čárka navíc) otevři soubor a oprav ho
InvalidOperationException u GetInt32 hodnota není číslo čti ji GetString(), nebo oprav soubor
soubor je pořád prázdný zápis se nespustil, nebo se přepsal zkontroluj WriteAllText vs AppendAllText
starý obsah zmizel WriteAllText soubor přepisuje použij AppendAllText
CS0103: The name 'JsonSerializer' does not exist chybí using nahoru přidej using System.Text.Json;
v souboru jsou kódy \u010D výchozí escapování češtiny nevadí, při čtení vyjde správně

3. Cvičení

Jak dnes cvičení dělat

Všechna dnešní cvičení pracují se soubory, takže se nespouštějí v prohlížeči. Postup je pokaždé stejný: kód si zkopíruj do Program.cs ve svém projektu, spusť dotnet run a pak se podívej do složky projektu, jaké soubory tam přibyly. Nápovědy „💡 Poradit" fungují normálně — jen ti kód nevloží do editoru, ale poradí slovy.

Blok 14Cvičení 1 — Deník výpravy (15 min)3. Cvičení

Zadání:

Hrdina si vede deník. Ten musí přežít i vypnutí počítače.

  1. Založ soubor denik.txt a zapiš do něj první zápis
    💡 Poradit
    Zakládá se jedním řádkem: File.WriteAllText(CESTA, "Den 1: …\n");
    Pokud soubor neexistuje, vyrobí se. Pokud existuje, přepíše se.
    Znak \n na konci je odřádkování — bez něj se zápisy slijí.
  2. Přidej další tři zápisy, aniž bys ty předchozí smazal
    💡 Poradit
    Na přidávání je File.AppendAllText(CESTA, "Den 2: …\n");
    Kdybys znovu použil WriteAllText, zůstal by v souboru jen poslední zápis.
    Zkus to schválně a podívej se, co se stane — pochopíš to líp než z vysvětlení.
  3. Přečti celý deník a vypiš ho na obrazovku
    💡 Poradit
    string obsah = File.ReadAllText(CESTA); vrátí celý soubor jako jeden text.
    Pak ho stačí vypsat: Console.WriteLine(obsah);
    Pro jistotu se nejdřív zeptej: if (File.Exists(CESTA)).
  4. Vypiš zápisy očíslované a řekni, kolik jich je
    💡 Poradit
    string[] radky = File.ReadAllLines(CESTA); rozdělí soubor po řádcích.
    Počet zápisů je radky.Length.
    Očíslování: for (int i = 0; i < radky.Length; i++) Console.WriteLine($"{i + 1}. {radky[i]}");
  5. Vypiš jen ty zápisy, ve kterých se objeví hledané slovo
    💡 Poradit
    Hledané slovo si dej do proměnné: string HLEDANE = "skřet";
    V cyklu přes řádky se ptej: if (radek.ToLower().Contains(HLEDANE))
    ToLower() zajistí, že najdeš i „Skřet" s velkým S.
Úroveň Co má zvládnout
Minimum Body 1–3, deník se založí, doplní a vypíše
Rozšíření Bod 4, očíslované zápisy a jejich počet
Výzva Bod 5 a navíc počet nalezených zápisů
cviceni/cviceni1_denik.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Pro učitele — na co si dát pozor

Očekávaný výsledek: soubor denik.txt se čtyřmi řádky ve složce projektu, vypsaný deník, očíslované zápisy a dva nalezené zápisy se slovem „skřet".

Kde se zaseknou:

  • Čtyřikrát WriteAllText. V souboru zůstane jen poslední zápis. Je to nejlepší možná ukázka rozdílu — nechte je to udělat.
  • Chybějící \n. Celý deník se slije do jednoho řádku a ReadAllLines pak vrátí jediný prvek. Poznají to podle Zápisů celkem: 1.
  • Hledání s velkým písmenem. Bez ToLower() najdou jen jeden ze dvou zápisů. Dobrá příležitost mluvit o tom, že počítač je doslovný.
  • „Kde ten soubor je?" Nechte je vypsat Directory.GetCurrentDirectory() a soubor opravdu otevřít. Ten moment je pro spoustu dětí ten nejsilnější z celé hodiny.

Kdo je hotový dřív: ať program upraví tak, aby se při každém spuštění přidal jeden nový zápis s aktuálním datem (DateTime.Now.ToString("d.M.yyyy")) a deník tak rostl.

Otázka na závěr cvičení: „Proč je WriteAllText nebezpečnější než AppendAllText?" Odpověď: protože nenávratně přepíše, co v souboru bylo.

Řešení 1 — jedna z možných variant
reseni/reseni1_denik.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Blok 15Cvičení 2 — Síň slávy (20 min)3. Cvičení

Zadání:

Nejlepší výsledky musí přežít restart. Ulož je do souboru a zase načti.

  1. Zapiš do skore.txt čtyři řádky ve tvaru jmeno;skore
    💡 Poradit
    Nejdřív si připrav pole textů: string[] zaznamy = { "Arkon;1250", "Elara;980", … };
    Zapíšeš je najednou: File.WriteAllLines(CESTA, zaznamy);
    Každý prvek pole skončí na vlastním řádku.
  2. Načti řádky, rozřež je a ulož do seznamu n-tic
    💡 Poradit
    string[] casti = radek.Split(';'); rozřeže řádek na dva kusy.
    casti[0] je jméno, casti[1] je skóre — ale jako text.
    Na číslo ho převedeš: int.Parse(casti[1])
    A přidáš: tabulka.Add((casti[0], int.Parse(casti[1])));
  3. Seřaď žebříček od nejvyššího skóre a vypiš ho s pořadím
    💡 Poradit
    var serazeno = tabulka.OrderByDescending(h => h.skore).ToList();
    Pořadí vypíšeš klasickým for cyklem přes index.
    Zarovnání: {serazeno[i].jmeno.PadRight(8)} a {serazeno[i].skore,5}.
  4. Přidej nový výsledek a ulož celou tabulku zpátky do souboru
    💡 Poradit
    Nejdřív do seznamu: tabulka.Add(("Nová hrdinka", 1420));
    Pak z n-tic udělej zase řádky:
    var vystup = new List<string>();
    foreach (var h in serazeno) vystup.Add($"{h.jmeno};{h.skore}");
    A zapiš: File.WriteAllLines(CESTA, vystup);
  5. Vypiš statistiku: nejlepší, nejhorší a průměrné skóre
    💡 Poradit
    LINQ to umí jedním voláním:
    tabulka.Max(h => h.skore), tabulka.Min(h => h.skore), tabulka.Average(h => h.skore)
    Průměr je desetinné číslo, zaokrouhli ho: $"{prumer:F0}"
    Sloupec k výsledku: new string('█', h.skore / 200)
Úroveň Co má zvládnout
Minimum Body 1–2, řádky se načtou a rozřežou
Rozšíření Body 3–4, seřazený žebříček uložený zpátky
Výzva Bod 5 se statistikou a sloupcovým grafem
cviceni/cviceni2_skore.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Pro učitele — na co si dát pozor

Očekávaný výsledek: pět hrdinů seřazených od 1640 dolů, soubor skore.txt přepsaný do nového pořadí a statistika s průměrem 1279 bodů.

Kde se zaseknou:

  • int.Parse na textu s mezerou. Pokud si dopíšou záznam jako "Nyra; 1420", Parse spadne na FormatException. Řešení je int.Parse(casti[1].Trim()) — a je to dobrá lekce o tom, že data z venku bývají špinavá.
  • Zapomenutý .ToList(). OrderByDescending vrací IEnumerable, které nemá Count ani indexer [i].
  • Uložení nesetříděné tabulky. Zapíšou tabulka místo serazeno. Funguje to, ale soubor pak není seřazený — ať si ho otevřou a uvidí.
  • Dělení nulou u grafu. Kdo si zvolí dělitel podle nejmenšího skóre a to je nula, spadne. Ať používají pevnou konstantu.

Kdo je hotový dřív: ať program spustí dvakrát a vysvětlí, proč se tabulka podruhé neznásobila (protože WriteAllLines na začátku soubor přepíše). Pak ať to změní tak, aby se skóre opravdu hromadilo.

Otázka na závěr cvičení: „Proč se skóre v souboru musí převádět z textu na číslo?" Odpověď: v souboru je jen text; "980" > "1640" by se porovnalo podle abecedy.

Řešení 2 — jedna z možných variant
reseni/reseni2_skore.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Blok 16Cvičení 3 — Export postavy do JSONu (20 min)3. Cvičení

Zadání:

Textový soubor na postavu nestačí. Ulož ji ve formátu, kterému rozumí celý svět.

  1. Připrav postavu jako Dictionary<string, object>
    💡 Poradit
    Nahoru patří using System.Text.Json;
    Slovník se zapisuje takhle:
    var postava = new Dictionary<string, object> { ["jmeno"] = "Arkon", ["uroven"] = 3 };
    Hodnota smí být číslo, text i celý List<string>.
  2. Převeď postavu na JSON s odsazením a vypiš text
    💡 Poradit
    var nastaveni = new JsonSerializerOptions { WriteIndented = true };
    string text = JsonSerializer.Serialize(postava, nastaveni);
    Bez WriteIndented by byl celý JSON na jediném řádku.
  3. Ulož JSON do souboru a vypiš, kam ses ho uložil
    💡 Poradit
    File.WriteAllText("postava.json", text);
    Kde soubor skončil, zjistíš takhle:
    Console.WriteLine(Directory.GetCurrentDirectory());
    Otevři si ho pak v editoru — je to obyčejný text.
  4. Načti soubor zpátky a vypiš jméno, úroveň a životy
    💡 Poradit
    var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText(CESTA)) ?? new Dictionary<string, JsonElement>();
    Text vytáhneš .GetString() ?? "", číslo .GetInt32().
    Před čtením se zeptej if (File.Exists(CESTA)).
  5. Vypiš inventář a poskládej z toho kartu postavy
    💡 Poradit
    Seznam se prochází přes EnumerateArray:
    foreach (JsonElement polozka in data["inventar"].EnumerateArray())
    Uvnitř: Console.WriteLine(" • " + polozka.GetString());
    Rámeček karty poskládej ze znaků a jako v ukázce 4.
Úroveň Co má zvládnout
Minimum Body 1–3, JSON se vypíše a uloží
Rozšíření Bod 4, načtení zpátky a výpis vlastností
Výzva Bod 5 — hotová karta postavy i s inventářem
cviceni/cviceni3_export.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Pro učitele — na co si dát pozor

Očekávaný výsledek: vypsaný JSON, soubor postava.json ve složce projektu a na konci vykreslená karta postavy s inventářem o třech položkách.

Kde se zaseknou:

  • Chybějící using System.Text.Json;. Hláška CS0103: The name 'JsonSerializer' does not exist. Připomeňte, že using patří úplně nahoru.
  • \u010D v souboru. Bude to první, čeho si všimnou. Uklidněte je: je to normální escapování, po načtení vyjde „Meč" správně.
  • Deserialize vrací null. Bez ?? new Dictionary<…>() se objeví varování a potenciální NullReferenceException. Vysvětlete ?? jako „a když nic, tak tohle".
  • .GetInt32() na textové hodnotě. InvalidOperationException. Ať si soubor otevřou a podívají se, jestli je hodnota v uvozovkách.

Kdo je hotový dřív: ať si soubor ručně upraví v editoru (změní životy), program spustí znovu bez části, která postavu ukládá, a uvidí, že karta ukazuje jeho ruční změnu. To je aha moment celé hodiny.

Otázka na závěr cvičení: „Proč je JSON lepší než náš jmeno;skore?" Odpověď: unese vnořené seznamy, pojmenované klíče a přečte ho i cizí program.

Řešení 3 — jedna z možných variant
reseni/reseni3_export.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Blok 17Cvičení 4 — Bestiář ze souboru (20 min)3. Cvičení

Zadání:

Příšery nepatří do kódu, ale do datového souboru. Pak je můžeš měnit bez překládání hry.

  1. Vyrob bestiář jako seznam slovníků a ulož ho do bestiar.json
    💡 Poradit
    Jedna příšera je slovník, bestiář je jejich seznam:
    var bestiar = new List<Dictionary<string, object>>();
    bestiar.Add(new Dictionary<string, object> { ["jmeno"] = "Skřet", ["zivoty"] = 30, ["utok"] = 6 });
    Uložení je stejné jako u postavy: JsonSerializer.Serialize(bestiar, nastaveni).
  2. Načti soubor zpátky a vypiš jména všech příšer
    💡 Poradit
    Typ při čtení je seznam slovníků s JsonElement:
    var nactene = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(text) ?? new List<Dictionary<string, JsonElement>>();
    Pak stačí foreach (var prisera in nactene) Console.WriteLine(prisera["jmeno"].GetString());
  3. Vypiš přehlednou tabulku: jméno, životy, útok
    💡 Poradit
    Zarovnání drží tabulku pohromadě:
    $"{jmeno.PadRight(14)} {zivoty,4} HP {utok,3} ATK"
    Čísla čti přes prisera["zivoty"].GetInt32().
    Nad tabulku si vypiš hlavičku a čáru z .
  4. Najdi nejsilnější příšeru podle útoku
    💡 Poradit
    Klasický průchod s pamatováním nejlepšího:
    string nejJmeno = ""; a int nejUtok = -1;
    V cyklu: if (utok > nejUtok) { nejUtok = utok; nejJmeno = jmeno; }
    Začíná se od -1, aby první příšera vždycky vyhrála.
  5. Seřaď bestiář podle životů a dokresli sloupce
    💡 Poradit
    Nejdřív si přelij data do n-tic, ať se dobře řadí:
    var prehled = new List<(string jmeno, int zivoty, int utok)>();
    Pak: prehled.OrderByDescending(p => p.zivoty).ToList()
    Sloupec: new string('█', p.zivoty / 10)
Úroveň Co má zvládnout
Minimum Body 1–2, bestiář se uloží a načte
Rozšíření Body 3–4, tabulka a nejsilnější příšera
Výzva Bod 5 se seřazeným žebříčkem a sloupci
cviceni/cviceni4_bestiar.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Pro učitele — na co si dát pozor

Očekávaný výsledek: soubor bestiar.json s polem pěti objektů, vypsaná tabulka, Drak jako nejsilnější (25 ATK) a žebříček podle životů se sloupci.

Kde se zaseknou:

  • Špatný typ při čtení. Čekají Dictionary<…>, ale v souboru je pole, takže je potřeba List<Dictionary<string, JsonElement>>. Ať se podívají do souboru: začíná hranatou závorkou, tedy seznam.
  • nejUtok = 0 místo -1. U příšery s útokem 0 by se nikdy nevybrala. U našich dat to projde, ale je to dobrá příležitost mluvit o okrajových případech.
  • Dlouhý zápis new Dictionary<string, object> { … }. Působí to hrozivě. Zdůrazněte, že v 16. hodině to nahradí jedna třída Prisera.
  • Zapomenutý ?? "neznámá". Bez toho se objeví varování o null.

Kdo je hotový dřív: ať do bestiáře přidá klíč "odolnosti" jako List<string> a vypíše ho přes EnumerateArray. Nebo ať soubor upraví ručně a znovu spustí jen čtecí část.

Otázka na závěr cvičení: „Proč je lepší mít příšery v souboru než v kódu?" Odpověď: data se dají měnit bez zásahu do programu — i někým, kdo neumí programovat.

Řešení 4 — jedna z možných variant
reseni/reseni4_bestiar.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Blok 18Cvičení 5 — CsQuest: uložení hry (25 min)3. Cvičení

Zadání:

Tohle je to, kvůli čemu se celá hodina děje. Hra, která si tě pamatuje.

  1. Napiš metodu NovaHra(), která vrátí výchozí stav včetně klíče "verze"
    💡 Poradit
    Metoda vrací slovník:
    Dictionary<string, object> NovaHra() { return new Dictionary<string, object> { ["verze"] = VERZE, ["jmeno"] = "Arkon", … }; }
    Klíč "verze" je nejdůležitější — díky němu poznáš staré uložené hry.
  2. Napiš metodu Uloz(stav), která stav zapíše jako JSON
    💡 Poradit
    void Uloz(Dictionary<string, object> stav) { … }
    Uvnitř: string text = JsonSerializer.Serialize(stav, new JsonSerializerOptions { WriteIndented = true });
    a File.WriteAllText(CESTA, text);
    Na konec přidej hlášku, ať víš, že se uložilo.
  3. Když soubor neexistuje, založ novou hru; jinak ho načti
    💡 Poradit
    if (!File.Exists(CESTA)) { Uloz(NovaHra()); }
    Teprve potom čti:
    var stav = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText(CESTA)) ?? new Dictionary<string, JsonElement>();
  4. Zkontroluj verzi a při neshodě založ hru novou
    💡 Poradit
    Nejdřív se zeptej, jestli klíč vůbec existuje:
    if (!stav.ContainsKey("verze") || stav["verze"].GetInt32() != VERZE)
    Uvnitř zavolej Uloz(NovaHra()); a soubor načti znovu.
    Bez téhle pojistky se hra rozbije, jakmile změníš strukturu uložené hry.
  5. Odehraj jeden tah: úroveň nahoru, zlato, nový předmět — a ulož
    💡 Poradit
    Nejdřív si vytáhni, co v uložené hře je (GetString, GetInt32, EnumerateArray).
    Inventář přelij do obyčejného seznamu:
    var inventar = new List<string>(); foreach (JsonElement p in stav["inventar"].EnumerateArray()) inventar.Add(p.GetString() ?? "");
    Pak přidej předmět, poskládej nový slovník a zavolej Uloz.
Úroveň Co má zvládnout
Minimum Body 1–3, hra se založí a načte
Rozšíření Bod 4, kontrola verze
Výzva Bod 5 — po každém spuštění hrdina roste a mění se i soubor
cviceni/cviceni5_ulozena_hra.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Pro učitele — na co si dát pozor

Očekávaný výsledek: při prvním spuštění se založí nová hra (úroveň 1), při druhém už hrdina začíná na úrovni 2 a s 120 zlaťáky. Nechte je spustit program třikrát za sebou — ten rostoucí hrdina je odměna za celou hodinu.

Kde se zaseknou:

  • Čtení před vytvořením. Když dají Deserialize nad if (!File.Exists…), spadne to při prvním spuštění. Pořadí je zásadní.
  • Zapomenutá druhá Deserialize po obnově verze. Proměnná stav pak drží stará data. Projeví se to divnými čísly.
  • Inventář se nepřelije do List<string>. JsonElement nejde jen tak přidat do nového slovníku — potřebují obyčejný seznam textů.
  • Životy do mínusu. Po sedmi spuštěních jde zivoty / 10 do záporu a new string('█', záporné) spadne. Krásná příležitost pro Math.Max(0, …) — ať na to přijdou sami tím, že program spustí desetkrát.

Kdo je hotový dřív: ať přidá záložní kopii (csquest_save.bak.json) před každým uložením, nebo ať do stavu přidá klíč "pocet_spusteni", který se s každým během zvýší o jedna.

Otázka na závěr cvičení: „Proč se do uložené hry ukládá i číslo verze?" Odpověď: aby program poznal soubor ze staré verze a nespadl na chybějícím klíči.

Řešení 5 — jedna z možných variant
reseni/reseni5_ulozena_hra.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.

4. Domácí úkol

Blok 19Domácí úkol na příští hodinu4. Domácí úkol

Vyrob program „Kronika tvé postavy", který si pamatuje, co se dělo:

  • při každém spuštění načte kronika.json (a když neexistuje, založí ho),
  • přidá nový záznam s číslem spuštění, datem a nějakou událostí,
  • celou kroniku uloží zpátky do souboru,
  • vypíše všechny dosavadní záznamy očíslované pod sebou,
  • a na konci napíše, kolikrát už jsi program spustil.

Pošli Program.cs i výsledný kronika.json po nejméně třech spuštěních. Ať je na něm vidět, že si program opravdu pamatuje.

Pro učitele

Tenhle úkol se nedá udělat v prohlížeči — potřebuje skutečný soubor na disku. Je to schválně: cílem je, aby si dítě doma otevřelo VS Code, spustilo dotnet run třikrát za sebou a vidělo, jak soubor roste. Ten zážitek „program si to pamatuje i po vypnutí" je pointa celé hodiny.

Kdo nemá doma .NET, ať úkol udělá ve škole o přestávce nebo na konci hodiny — stačí pět minut. Případně mu zadejte variantu bez souboru: kronika jen v List<string> a na konci výpis. Pointa se ztratí, ale procvičí si JSON.

Na začátku příští hodiny nechte dva tři žáky ukázat jejich kronika.json v textovém editoru. Když třída uvidí ten samý text, který program napsal, pochopí, že JSON není magie, ale obyčejný soubor.

Zobrazit ukázkové řešení domácího úkolu

Toto je jedna z možných variant, jak mohl úkol dopadnout. Tvoje řešení se může lišit a to je v pořádku.

domaci/domaci_ukol14.cs
Tenhle kód potřebuje soubory, síť nebo vlákna, což v prohlížeči nejde — spusť ho na počítači příkazem dotnet run.
Blok 20Oficiální dokumentace4. Domácí úkol

Všechno, co jsme dnes použili, je popsané na Microsoft Learn. Odkazy jsou anglicky — klidně si je nech přeložit prohlížečem, kód je stejný v každém jazyce.

Téma Odkaz
Třída File learn.microsoft.com — File Class
File.WriteAllText learn.microsoft.com — File.WriteAllText
File.ReadAllLines learn.microsoft.com — File.ReadAllLines
Directory a Path learn.microsoft.com — Path.Combine
JSON v .NETu learn.microsoft.com — JSON serialization overview
JsonSerializer learn.microsoft.com — JsonSerializer Class
JsonElement learn.microsoft.com — JsonElement Struct

5. Umíme informatiku

Blok 21Co dnes umíš navíc5. Umíme informatiku

Dneškem se z tvých programů staly aplikace. Rozdíl je jediný, zato zásadní: aplikace si pamatuje, co se stalo.

  • Umíš zapsat text do souboru a víš, čím se liší WriteAllText od AppendAllText.
  • Umíš soubor přečíst celý i po řádcích a rozřezat ho na data.
  • Vždycky se zeptáš File.Exists, než začneš číst.
  • Víš, co je JSON, umíš ho přečíst očima i programem.
  • Používáš JsonSerializer.Serialize a Deserialize a rozumíš tomu, že jde o cestu tam a zpátky mezi pamětí a diskem.
  • A umíš uložit a načíst rozehranou hru — včetně kontroly verze, což dělají i opravdové hry.

Podívej se na to takhle: každá aplikace v tvém mobilu dělá přesně tohle. Ukládá si nastavení, postup ve hře, rozepsané zprávy. Formát je většinou JSON a metody se jmenují skoro stejně. Dneska jsi otevřel dveře, za kterými programy přestávají být hračkou na jedno spuštění.

Příště začneme skládat CsQuest dohromady — mapu z dvanácté hodiny, seřazený inventář ze třinácté a uloženou hru z dnešní.