Kluczowe umiejętności egzaminacyjne INF.04 na motywie gry

Często uczniowie pytają:
„Proszę pana, a to nam się przyda na egzamin?”

No to pogadajmy normalnie, bez ściemy.

Ta gra, którą robimy (CyberRunner), to nie jest zabawa dla zabawy.
To jest konkretny zestaw umiejętności, które egzamin INF.04 sprawdza – tylko że ubrane w formę gry, a nie nudnego formularza z trzema TextBoxami.


1. Obsługa klawiatury i zdarzeń

W grze:

  • reagujesz na KeyDown i KeyUp
  • obsługujesz kilka klawiszy naraz (ruch + skok + strzał)
  • sterowanie nie jest „na klik”, tylko ciągłe

Na egzaminie:

  • to jest dokładnie obsługa zdarzeń
  • INF.04 uwielbia sytuacje typu: po naciśnięciu klawisza wykonaj akcję

Tu nie uczysz się teorii.
Tu robisz to naprawdę.


2. Timer i pętla gry = serce aplikacji

W grze:

  • DispatcherTimer
  • pętla, która wykonuje się co ~16 ms
  • osobny timer do bonusów (magazynki)

Na egzaminie:

  • cykliczne wykonywanie kodu
  • aktualizacja stanu aplikacji w czasie
  • animacje, liczniki, zegary

To jest 100% INF.04, tylko że w wersji, która ma sens.


3. Kolizje i współrzędne (czyli myślenie logiczne)

W grze:

  • sprawdzasz, czy gracz dotknął wroga
  • czy pocisk trafił
  • czy bonus został zebrany

Na egzaminie:

  • instrukcje warunkowe
  • praca na danych
  • logika „jeśli – to – wtedy”

Egzamin nie pyta: „czy znasz Rect”
Egzamin sprawdza: czy umiesz myśleć warunkami.


4. Stan aplikacji (bardzo ważne!)

W grze masz:

  • poziom
  • punkty
  • życia
  • ammo
  • nietykalność

To wszystko to stan aplikacji.

Na egzaminie:

  • bardzo często jest: zależnie od wartości zmiennej zrób coś
  • odblokowywanie funkcji
  • ograniczenia

Tutaj to widać czarno na białym.


5. Progresja – coś się dzieje „od któregoś poziomu”

W grze:

  • od levelu 3 → nowy wróg
  • od levelu 4 → druga linia
  • od levelu 8 → UFO
  • z każdym levelem gra przyspiesza

Na egzaminie:

  • warunki typu if (poziom >= ...)
  • zmiana zachowania programu w trakcie działania

To jest dokładnie ten sam mechanizm, tylko w grze jest bardziej oczywisty.


6. Praca na kontrolkach (WPF w praktyce)

W grze:

  • Canvas
  • Rectangle
  • TranslateTransform
  • Visibility

Na egzaminie:

  • manipulowanie kontrolkami
  • zmiana widoczności
  • dynamiczny interfejs

Różnica?
Tutaj widzisz efekt od razu, a nie zgadujesz, czy coś działa.


7. Listy i obiekty (pociski)

W grze:

  • lista List<Pocisk>
  • własna klasa
  • dodawanie i usuwanie elementów

Na egzaminie:

  • kolekcje
  • obiekty
  • operacje na liście

To jest bardzo często punktowane, a uczniowie zwykle się tego boją.
Tutaj – robią to naturalnie.


8. Metody i porządek w kodzie

W grze:

  • DodajPunkty()
  • Traf()
  • ObsluzPociski()
  • SprawdzKolizje()

Na egzaminie:

  • podział programu na logiczne części
  • czytelność
  • brak „spaghetti code”

Egzaminator naprawdę to docenia.


9. Matematyka, ale taka normalna

W grze:

  • grawitacja
  • sinus dla UFO
  • przyspieszanie gry o 5%

Na egzaminie:

  • proste obliczenia
  • zastosowanie matematyki w kodzie

Nie wzory z tablicy.
Matematyka, która ma sens.


Podsumowując – na spokojnie

Ta gra:

  • nie jest „bajerem”
  • nie jest stratą czasu
  • nie jest oderwana od egzaminu

To jest:
praktyczny INF.04
logika + UI + zdarzenia
dokładnie to, co egzamin sprawdza

Jak ktoś ogarnia tę grę i rozumie, co tu się dzieje,
to egzamin INF.04 przestaje być straszny.

A to chyba najlepszy cel nauki, nie?
To teraz przejdźmy do kodu. Nie ma tu MVVM, to już dobrze znacie, i to będzie wasze zadanie, by dostosować tę gre do standardu egzaminacyjnego MVVM. Ja wam daję Algorytm (stworzony dla zabicia czasu w pociągu :)), a wasze zadanie to utworzyć z tego profesjonalną apkę.

No to do dzieła.

using System;
using System.Collections.Generic;
using System.Linq; // Potrzebne do obsługi rankingu (metody Append, OrderBy)
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace CyberRunner
{
    public partial class MainWindow : Window
    {
        // ==========================================================
        // TIMERY - Mechanizm czasu w grze (INF.04 - cykliczność)
        // ==========================================================
        // DispatcherTimer działa w tym samym wątku co UI, co ułatwia aktualizację grafiki.
        DispatcherTimer zegarGry = new DispatcherTimer();      // Główna pętla gry (ruch, grawitacja)
        DispatcherTimer zegarMagazynku = new DispatcherTimer(); // Odpowiada za cykliczne pojawianie się amunicji
        DispatcherTimer zegarZycia = new DispatcherTimer();     // Odpowiada za cykliczne pojawianie się apteczek

        // Statystyki sesji dla podsumowania
        DateTime czasRozpoczecia;
        int punktyCalkowite = 0; // Suma punktów ze wszystkich poziomów

        // ==========================================================
        // STAŁE - Wartości, które ułatwiają zarządzanie poziomami
        // ==========================================================
        const double Y_DOL = 310;            // Pozycja gracza na dolnym pasie
        const double Y_GORA = 170;           // Pozycja pasu górnego (platformy)
        const int PUNKTY_NA_POZIOM = 12;      // Ile punktów trzeba zdobyć, by wejść na wyższy level

        // ==========================================================
        // GRACZ - Fizyka i stan
        // ==========================================================
        double graczX = 150;                 // Pozycja pozioma
        double graczY = Y_DOL;               // Pozycja pionowa
        double grawitacja = 0;               // Siła ciągnąca gracza w dół (lub pęd skoku)

        bool ruchLewo, ruchPrawo, zejscieWDol; // Flagi sterowania (czy klawisz jest wciśnięty)
        bool skok, podwojnySkok;               // Zmienne do obsługi logiki skakania
        int kierunek = 1;                    // 1 = prawo, -1 = lewo (do orientacji pocisków)
        DateTime ostatniSkok = DateTime.MinValue; // Potrzebne do wykrycia czasu między kliknięciami (double jump)

        // ==========================================================
        // WROGOWIE - Statystyki (HP i pozycje)
        // ==========================================================
        double czerwonyDolX = 900;           // Start poza ekranem z prawej
        double czerwonyGoraX = 1300;
        int hpCzerwonyDol = 4;               // 4 hity do zabicia
        int hpCzerwonyGora = 4;

        double zoltyDolX = -300;             // Start poza ekranem z lewej (idą w prawo)
        double zoltyGoraX = -700;
        int hpZoltyDol = 8;                  // 8 hitów do zabicia (silniejszy wróg)
        int hpZoltyGora = 8;

        // ==========================================================
        // UFO - Przeciwnik specjalny (Level 8+)
        // ==========================================================
        double ufoX = 1100;
        double ufoFaza = 0;                  // Zmienna do obliczeń funkcji sinus (ruch falisty)
        double ufoAmplituda = 60;            // Jak wysoko/nisko lata UFO

        // ==========================================================
        // BONUSY
        // ==========================================================
        double ammoX = 900;
        bool ammoAktywne = false;

        double zycieX = 1200;
        bool zycieAktywne = false;

        // ==========================================================
        // STAN GRY - Zmienne HUD
        // ==========================================================
        int poziom = 1;
        int punkty = 0;
        int zycia = 30;
        int ammo = 100;
        double predkosc = 6;                 // Globalna prędkość przesuwu obiektów
        int nietykalnosc = 0;                // Licznik klatek po otrzymaniu obrażeń

        // ==========================================================
        // POCISKI - Zarządzanie obiektami dynamicznymi
        // ==========================================================
        class Pocisk
        {
            public Rectangle Ksztalt;        // Wygląd pocisku na ekranie
            public int Kierunek;             // W którą stronę leci
        }
        List<Pocisk> pociski = new();        // Kolekcja wszystkich pocisków w locie

        public MainWindow()
        {
            InitializeComponent();
            Plansza.Focus(); // Ustawienie fokusu, aby okno przechwytywało klawisze od razu

            // Konfiguracja głównego zegara (ok. 60 klatek na sekundę)
            zegarGry.Interval = TimeSpan.FromMilliseconds(16);
            zegarGry.Tick += PetlaGry;

            // Konfiguracja timerów dla bonusów
            zegarMagazynku.Interval = TimeSpan.FromSeconds(30);
            zegarMagazynku.Tick += (s, e) => AktywujAmmo();

            zegarZycia.Interval = TimeSpan.FromSeconds(45);
            zegarZycia.Tick += (s, e) => AktywujZycie();

            // Pierwsze uruchomienie gry
            RestartGry();
        }

        // Metoda inicjalizująca stan początkowy (idealna do nauki resetowania obiektów)
        void RestartGry()
        {
            // Reset parametrów gracza i punktacji
            poziom = 1; punkty = 0; punktyCalkowite = 0; zycia = 30; ammo = 100; predkosc = 6;
            graczX = 150; graczY = Y_DOL; grawitacja = 0;
            czasRozpoczecia = DateTime.Now;

            // Czyszczenie pocisków z ekranu (XAML) i z pamięci (Lista)
            foreach (var p in pociski) Plansza.Children.Remove(p.Ksztalt);
            pociski.Clear();

            // Przywrócenie wrogów i bonusów do stanu początkowego
            czerwonyDolX = 900; zoltyDolX = -300; ufoX = 1100;
            hpCzerwonyDol = 4; hpZoltyDol = 8;
            ammoAktywne = false; zycieAktywne = false;
            BonusAmmo.Visibility = Visibility.Collapsed;
            BonusZycie.Visibility = Visibility.Collapsed;

            zegarGry.Start();
            zegarMagazynku.Start();
            zegarZycia.Start();
            AktualizujHUD();
        }

        // ==========================================================
        // GŁÓWNA PĘTLA GRY - Wykonywana co 16ms
        // ==========================================================
        void PetlaGry(object sender, EventArgs e)
        {
            // ---- RUCH GRACZA ----
            if (ruchLewo) { graczX -= 8; kierunek = -1; }
            if (ruchPrawo) { graczX += 8; kierunek = 1; }

            // ---- GRAWITACJA I SKOK ----
            // Gracz zawsze "spada", chyba że grawitacja jest ujemna (wtedy leci w górę - skok)
            graczY += grawitacja;
            grawitacja += 1.6; // Przyciąganie ziemskie (zwiększa prędkość spadania)

            // Kolizja z dolną podłogą
            if (graczY >= Y_DOL)
            {
                graczY = Y_DOL;
                grawitacja = 0;
                skok = false;
                podwojnySkok = false;
            }

            // Obsługa górnej platformy (od poziomu 4)
            if (poziom >= 4)
            {
                PodlogaGora.Visibility = Visibility.Visible;
                // Warunek lądowania na górnej linii:
                // Gracz musi spadać (grawitacja > 0) i być blisko wysokości platformy
                if (!zejscieWDol && graczY >= Y_GORA - 60 && graczY < Y_GORA && grawitacja > 0)
                {
                    graczY = Y_GORA - 60;
                    grawitacja = 0;
                    skok = false;
                    podwojnySkok = false;
                }
            }

            // Szybkie schodzenie w dół (klawisz DOWN)
            if (zejscieWDol && graczY < Y_DOL)
                grawitacja = 6;

            // Aktualizacja pozycji grafiki gracza na ekranie
            TransformGracz.X = graczX;
            TransformGracz.Y = graczY;

            // ---- LOGIKA PRZECIWNIKÓW (Przesuwanie i resetowanie pozycji) ----
            czerwonyDolX -= predkosc;
            if (czerwonyDolX < -60) { czerwonyDolX = 900; hpCzerwonyDol = 4; }
            TransformCzerwonyDol.X = czerwonyDolX;
            TransformCzerwonyDol.Y = Y_DOL;

            if (poziom >= 4)
            {
                czerwonyGoraX -= predkosc;
                if (czerwonyGoraX < -60) { czerwonyGoraX = 1100; hpCzerwonyGora = 4; }
                TransformCzerwonyGora.X = czerwonyGoraX;
                TransformCzerwonyGora.Y = Y_GORA - 60;
                WrogCzerwonyGora.Visibility = Visibility.Visible;
            }

            if (poziom >= 2)
            {
                zoltyDolX += predkosc * 0.8;
                if (zoltyDolX > 900) { zoltyDolX = -300; hpZoltyDol = 8; }
                TransformZoltyDol.X = zoltyDolX;
                TransformZoltyDol.Y = Y_DOL - 10;
                WrogZoltyDol.Visibility = Visibility.Visible;
            }

            if (poziom >= 4)
            {
                zoltyGoraX += predkosc * 0.8;
                if (zoltyGoraX > 1100) { zoltyGoraX = -600; hpZoltyGora = 8; }
                TransformZoltyGora.X = zoltyGoraX;
                TransformZoltyGora.Y = Y_GORA - 70;
                WrogZoltyGora.Visibility = Visibility.Visible;
            }

            // ---- LOGIKA BONUSÓW ----
            if (ammoAktywne)
            {
                ammoX -= predkosc;
                TransformBonusAmmo.X = ammoX;
                TransformBonusAmmo.Y = Y_DOL - 40;
                if (ammoX < -60) { ammoAktywne = false; BonusAmmo.Visibility = Visibility.Collapsed; }
            }

            if (zycieAktywne)
            {
                zycieX -= predkosc;
                TransformBonusZycie.X = zycieX;
                TransformBonusZycie.Y = Y_DOL - 40;
                if (zycieX < -60) { zycieAktywne = false; BonusZycie.Visibility = Visibility.Collapsed; }
            }

            // ---- LOGIKA UFO (Level 8+) ----
            if (poziom >= 8)
            {
                UFO.Visibility = Visibility.Visible;
                ufoX -= predkosc;
                if (ufoX < -100) ufoX = 1100;
                ufoFaza += 0.05; // Zmiana kąta dla sinusa
                TransformUFO.X = ufoX;
                // Math.Sin tworzy płynny ruch góra-dół (wartości od -1 do 1)
                TransformUFO.Y = 220 + Math.Sin(ufoFaza) * ufoAmplituda;
            }

            // Wywołanie metod pomocniczych
            ObsluzPociski();
            SprawdzKolizje();
            AktualizujNietykalnosc();
        }

        // ==========================================================
        // POCISKI - Naprawiona punktacja i HP
        // ==========================================================
        void ObsluzPociski()
        {
            // Iterujemy od końca listy, bo będziemy usuwać elementy w trakcie pętli (INF.04 - kolekcje)
            for (int i = pociski.Count - 1; i >= 0; i--)
            {
                var p = pociski[i];
                double x = Canvas.GetLeft(p.Ksztalt) + 22 * p.Kierunek;
                Canvas.SetLeft(p.Ksztalt, x);

                // Tworzymy prostokąt kolizji dla pocisku
                Rect r = new Rect(x, Canvas.GetTop(p.Ksztalt), 20, 6);

                // Trafienie Czerwonego (Dół) - 4 HP, 1 Punkt
                if (r.IntersectsWith(new Rect(czerwonyDolX, Y_DOL, 40, 60)))
                {
                    hpCzerwonyDol--;
                    UsunPocisk(i);
                    if (hpCzerwonyDol <= 0) { DodajPunkty(1); czerwonyDolX = 900; hpCzerwonyDol = 4; }
                    continue; // Pocisk zniknął, sprawdzamy następny
                }

                // Trafienie Czerwonego (Góra) - 4 HP, 1 Punkt
                if (poziom >= 4 && r.IntersectsWith(new Rect(czerwonyGoraX, Y_GORA - 60, 40, 60)))
                {
                    hpCzerwonyGora--;
                    UsunPocisk(i);
                    if (hpCzerwonyGora <= 0) { DodajPunkty(1); czerwonyGoraX = 1100; hpCzerwonyGora = 4; }
                    continue;
                }

                // Trafienie Żółtego (Dół) - 8 HP, 2 Punkty
                if (poziom >= 2 && r.IntersectsWith(new Rect(zoltyDolX, Y_DOL - 10, 40, 70)))
                {
                    hpZoltyDol--;
                    UsunPocisk(i);
                    if (hpZoltyDol <= 0) { DodajPunkty(2); zoltyDolX = -300; hpZoltyDol = 8; }
                    continue;
                }

                // Trafienie Żółtego (Góra) - 8 HP, 2 Punkty
                if (poziom >= 4 && r.IntersectsWith(new Rect(zoltyGoraX, Y_GORA - 70, 40, 70)))
                {
                    hpZoltyGora--;
                    UsunPocisk(i);
                    if (hpZoltyGora <= 0) { DodajPunkty(2); zoltyGoraX = -600; hpZoltyGora = 8; }
                    continue;
                }

                // Usuwanie pocisków poza ekranem (optymalizacja)
                if (x < -40 || x > 900) UsunPocisk(i);
            }
        }

        // Zarządzanie punktami i wywołanie sprawdzenia poziomu
        void DodajPunkty(int ile)
        {
            punkty += ile;
            punktyCalkowite += ile;
            SprawdzLevel();
            AktualizujHUD();
        }

        void UsunPocisk(int i)
        {
            Plansza.Children.Remove(pociski[i].Ksztalt); // Usuń z ekranu (XAML)
            pociski.RemoveAt(i);                        // Usuń z pamięci (Lista)
        }

        // ==========================================================
        // KOLIZJE GRACZA - Czy gracz dotknął wroga lub bonusu?
        // ==========================================================
        void SprawdzKolizje()
        {
            Rect g = new Rect(graczX, graczY, 40, 60);

            // Kolizje z przeciwnikami (zróżnicowane DMG)
            if (g.IntersectsWith(new Rect(czerwonyDolX, Y_DOL, 40, 60))) Traf(1);
            if (poziom >= 4 && g.IntersectsWith(new Rect(czerwonyGoraX, Y_GORA - 60, 40, 60))) Traf(1);
            if (poziom >= 2 && g.IntersectsWith(new Rect(zoltyDolX, Y_DOL - 10, 40, 70))) Traf(2);
            if (poziom >= 4 && g.IntersectsWith(new Rect(zoltyGoraX, Y_GORA - 70, 40, 70))) Traf(2);

            // Kolizja z UFO - pobieramy pozycję bezpośrednio z Transformacji
            if (poziom >= 8)
            {
                if (g.IntersectsWith(new Rect(TransformUFO.X, TransformUFO.Y, 50, 30))) Traf(4);
            }

            // Zbieranie amunicji
            if (ammoAktywne && g.IntersectsWith(new Rect(ammoX, Y_DOL - 40, 40, 40)))
            {
                ammo += 50;
                ammoAktywne = false;
                BonusAmmo.Visibility = Visibility.Collapsed;
                AktualizujHUD();
            }

            // Zbieranie życia (teraz dodaje 5 HP, by uczeń poczuł różnicę)
            if (zycieAktywne && g.IntersectsWith(new Rect(zycieX, Y_DOL - 40, 40, 40)))
            {
                zycia += 5;
                zycieAktywne = false;
                BonusZycie.Visibility = Visibility.Collapsed;
                AktualizujHUD();
            }
        }

        // Reakcja na otrzymanie obrażeń
        void Traf(int dmg)
        {
            if (nietykalnosc > 0) return; // Jeśli gracz miga, nie traci HP
            zycia -= dmg;
            nietykalnosc = 60; // Okres ochronny (ok. 1 sekunda przy 60 FPS)
            AktualizujHUD();
            if (zycia <= 0) WyswietlKoniecGry();
        }

        // Ekran Końcowy (INF.04 - Podsumowanie pracy programu, ranking i czas)
        void WyswietlKoniecGry()
        {
            zegarGry.Stop(); zegarMagazynku.Stop(); zegarZycia.Stop();

            TimeSpan czasGry = DateTime.Now - czasRozpoczecia;

            // Generowanie statycznego rankingu z udziałem gracza
            string[] nicki = { "CyberMistrz", "BitBuster", "KernelPan", "WPF_Pro", "NullPtr", "INF_04_King", "Admin", "User1", "PlayerZero", "Anon" };
            Random rnd = new Random();
            var ranking = nicki.Select(imie => new { Imie = imie, Wynik = rnd.Next(10, 500) })
                                .Append(new { Imie = "TY", Wynik = punktyCalkowite })
                                .OrderByDescending(x => x.Wynik)
                                .Take(10)
                                .ToList();

            string rankingTekst = "TOP 10 GRACZY:\n";
            for (int i = 0; i < ranking.Count; i++)
                rankingTekst += $"{i + 1}. {ranking[i].Imie} - {ranking[i].Wynik} pkt\n";

            string podsumowanie = $"KONIEC GRY!\n\n" +
                                  $"Czas przetrwania: {czasGry.Minutes}m {czasGry.Seconds}s\n" +
                                  $"Zdobyte punkty łącznie: {punktyCalkowite}\n\n" +
                                  rankingTekst + "\n" +
                                  "Czy chcesz zagrać ponownie?";

            var decyzja = MessageBox.Show(podsumowanie, "CyberRunner - Wynik", MessageBoxButton.YesNo, MessageBoxImage.Information);

            if (decyzja == MessageBoxResult.Yes) RestartGry();
            else Application.Current.Shutdown();
        }

        // Wizualny efekt nietykalności (miganie Opacity)
        void AktualizujNietykalnosc()
        {
            if (nietykalnosc > 0)
            {
                nietykalnosc--;
                Gracz.Opacity = nietykalnosc % 10 < 5 ? 0.3 : 1; // Naprzemienna zmiana przezroczystości
            }
            else Gracz.Opacity = 1;
        }

        // ==========================================================
        // PROGRESJA - Zmiana poziomu trudności
        // ==========================================================
        void SprawdzLevel()
        {
            if (punkty >= PUNKTY_NA_POZIOM)
            {
                punkty = 0;
                poziom++;
                predkosc *= 1.05; // Gra przyspiesza o 5% z każdym poziomem
            }
            AktualizujHUD();
        }

        // Aktywacja bonusów przez Timery
        void AktywujAmmo() { ammoAktywne = true; ammoX = 900; BonusAmmo.Visibility = Visibility.Visible; }
        void AktywujZycie() { zycieAktywne = true; zycieX = 1200; BonusZycie.Visibility = Visibility.Visible; }

        // ==========================================================
        // STEROWANIE - Obsługa klawiatury
        // ==========================================================
        void Plansza_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Left) ruchLewo = true;
            if (e.Key == Key.Right) ruchPrawo = true;
            if (e.Key == Key.Down) zejscieWDol = true;

            if (e.Key == Key.Up)
            {
                DateTime teraz = DateTime.Now;
                // Pierwszy skok (możliwy tylko gdy gracz stoi na ziemi/platformie)
                if (!skok)
                {
                    grawitacja = -22; // Nadajemy pęd w górę
                    skok = true;
                    podwojnySkok = true;
                }
                // Drugi skok (double jump) - jeśli kliknięto szybko drugi raz
                else if ((teraz - ostatniSkok).TotalMilliseconds < 250 && podwojnySkok)
                {
                    grawitacja = -32; // Silniejszy wybicie
                    podwojnySkok = false;
                }
                ostatniSkok = teraz;
            }

            if (e.Key == Key.Space) Strzel();
        }

        void Plansza_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Left) ruchLewo = false;
            if (e.Key == Key.Right) ruchPrawo = false;
            if (e.Key == Key.Down) zejscieWDol = false;
        }

        // ==========================================================
        // MECHANIKA STRZAŁU
        // ==========================================================
        void Strzel()
        {
            if (ammo <= 0) return;
            ammo--;

            // Tworzenie dynamicznie kontrolki pocisku
            Rectangle r = new Rectangle { Width = 20, Height = 6, Fill = Brushes.Cyan };
            // Ustawienie pozycji startowej pocisku względem gracza
            Canvas.SetLeft(r, graczX + (kierunek == 1 ? 40 : -20));
            Canvas.SetTop(r, graczY + 25);

            Plansza.Children.Add(r); // Dodanie do Canvasa
            pociski.Add(new Pocisk { Ksztalt = r, Kierunek = kierunek }); // Dodanie do listy logiki
            AktualizujHUD();
        }

        // Odświeżanie napisów na ekranie (HUD)
        void AktualizujHUD()
        {
            TekstPoziom.Text = $"POZIOM: {poziom}";
            TekstPunkty.Text = $"PUNKTY: {punkty}/{PUNKTY_NA_POZIOM}";
            TekstZycia.Text = $"ŻYCIA: {zycia}";
            TekstAmmo.Text = $"AMMO: {ammo}";
        }
    }
}
<Window x:Class="CyberRunner.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CyberRunner"
        Width="800" Height="450"
        Background="#050505"
        WindowStartupLocation="CenterScreen">

    <Canvas Name="Plansza"
            Focusable="True"
            KeyDown="Plansza_KeyDown"
            KeyUp="Plansza_KeyUp">

        <!-- ================= PODŁOGI ================= -->
        <!-- Dolna linia – od początku gry -->
        <Rectangle Width="800" Height="5"
                   Fill="#00f2ff"
                   Canvas.Top="370"/>

        <!-- Górna linia – aktywna od levelu 4 -->
        <Rectangle Name="PodlogaGora"
                   Width="800" Height="5"
                   Fill="#7CFF00"
                   Canvas.Top="170"
                   Visibility="Collapsed"/>

        <!-- ================= GRACZ ================= -->
        <Rectangle Name="Gracz"
                   Width="40" Height="60"
                   Fill="#00d2ff">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformGracz"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= WROGOWIE – DÓŁ ================= -->

        <!-- Czerwony wróg – dół -->
        <Rectangle Name="WrogCzerwonyDol"
                   Width="40" Height="60"
                   Fill="#ff0058">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformCzerwonyDol"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- Żółty wróg – dół -->
        <Rectangle Name="WrogZoltyDol"
                   Width="40" Height="70"
                   Fill="Gold"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformZoltyDol"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= WROGOWIE – GÓRA ================= -->

        <!-- Czerwony wróg – góra -->
        <Rectangle Name="WrogCzerwonyGora"
                   Width="40" Height="60"
                   Fill="#ff0058"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformCzerwonyGora"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- Żółty wróg – góra -->
        <Rectangle Name="WrogZoltyGora"
                   Width="40" Height="70"
                   Fill="Gold"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformZoltyGora"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= KOLCE / SOPL E ================= -->
        <Rectangle Name="Kolce"
                   Width="60" Height="30"
                   Fill="#ff3333"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformKolce"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= UFO ================= -->
        <Rectangle Name="UFO"
                   Width="50" Height="30"
                   Fill="#baff00"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformUFO"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= BONUSY ================= -->

        <!-- Biały magazynek – +50 ammo -->
        <Rectangle Name="BonusAmmo"
                   Width="40" Height="40"
                   Fill="White"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformBonusAmmo"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- Fioletowe życie – +1 HP -->
        <Rectangle Name="BonusZycie"
                   Width="40" Height="40"
                   Fill="Purple"
                   Visibility="Collapsed">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="TransformBonusZycie"/>
            </Rectangle.RenderTransform>
        </Rectangle>

        <!-- ================= HUD ================= -->
        <StackPanel Canvas.Left="20" Canvas.Top="20">
            <TextBlock Name="TekstPoziom"
                       Foreground="White"
                       FontSize="22"/>
            <TextBlock Name="TekstPunkty"
                       Foreground="#00d2ff"
                       FontSize="18"/>
            <TextBlock Name="TekstZycia"
                       Foreground="#ff4d4d"
                       FontSize="18"/>
            <TextBlock Name="TekstAmmo"
                       Foreground="White"
                       FontSize="18"/>
        </StackPanel>

    </Canvas>
</Window>