Summer APPS

Lipiec 2025 r.

Wakacyjne Warsztaty Programowania –
ZS nr 1 w Działdowie

W ramach letniej propozycji edukacyjnej dla uczniów klas 7–8 szkół podstawowych organizujemy Wakacyjne Warsztaty Programowania, których głównym celem jest nauka podstaw języka C# poprzez praktyczne tworzenie prostej gry komputerowej.


Etap 1 – Wprowadzenie do Visual Studio, WPF i przygotowanie projektu gry „JajkaWilk”

👶 Na początek – Co to wszystko znaczy?

Visual Studio 2022

To specjalny program (tzw. środowisko programistyczne), w którym tworzy się aplikacje, gry i strony internetowe.

Można w nim:

  • pisać kod,
  • projektować okna,
  • testować aplikacje.

👉 To jak warsztat dla programisty – wszystko w jednym miejscu.

C# (C-Sharp)

To język programowania, czyli sposób, w jaki mówimy komputerowi, co ma robić.

👨‍💻 Przykład:

int liczba = 5;

To znaczy: „utwórz zmienną o nazwie liczba i przypisz jej wartość 5”.

C# jest prosty, nowoczesny i często używany do:

  • gier,
  • aplikacji okienkowych,
  • stron internetowych.

WPF

WPF = Windows Presentation Foundation. To technologia do tworzenia ładnych okienkowych aplikacji na Windowsa – takich jak kalkulatory, gry, formularze.

👉 Dzięki WPF robimy:

  • interfejs (czyli wygląd okna),
  • obsługę kliknięć, klawiszy, dźwięków itp.

XAML

To specjalny język do opisywania, jak ma wyglądać okno aplikacji. To jak HTML, ale dla aplikacji na Windows.

👀 Przykład:

<Button Content="Kliknij mnie" Width="100" Height="30"/>

To przycisk z napisem „Kliknij mnie”.


🪜 Krok po kroku: Tworzenie projektu i wczytanie grafik


Krok 1: Utwórz nowy projekt

  1. Uruchom Visual Studio 2022.
  2. Kliknij Utwórz nowy projekt.
  3. Wyszukaj: WPF i wybierz: ✅ Aplikacja WPF (.NET Framework)
  4. Kliknij Dalej.
  5. Wpisz nazwę projektu: JajkaWilk.
  6. Kliknij Utwórz.

Krok 2: Dodaj folder Assets

  1. W oknie Eksplorator rozwiązań (Solution Explorer) po prawej stronie:
  2. Kliknij prawym przyciskiem myszy na nazwę projektu JajkaWilk.
  3. Wybierz Dodaj → Nowy folder.
  4. Nazwij go Assets (to będzie miejsce na grafiki do gry).

Krok 3: Dodaj pliki graficzne do folderu Assets

  1. Kliknij prawym przyciskiem na folder Assets.
  2. Wybierz Dodaj → Istniejący element…
  3. Wybierz pliki (od nauczyciela), np.:
    • tlogry.png (tło gry)
    • egg1.png (jajko)
    • wilkdol1.png, wilkgora1.png (grafiki wilka)
  4. Kliknij Dodaj.

Krok 4: Ustaw właściwości każdego pliku graficznego

  1. Kliknij jeden raz na grafikę (np. egg1.png).
  2. W oknie Właściwości (na dole lub z prawej – jeśli go nie widać, naciśnij F4):
    • Akcja kompilacji → ustaw na: Zawartość
    • Kopiuj do katalogu wyjściowego → ustaw na: Zawsze kopiuj

🔁 Powtórz to dla każdej grafiki.

GRATULACJE! Właśnie utworzyłeś swoj projekt w Visual Studio 2022. Gotowy na etap2?


Etap 2 – wersja z Projektantem i Przybornikiem (Toolbox)

Będziemy teraz zbudować planszę gry bez pisania XAML, używając projektanta wizualnego i ustawiając właściwości kontrolek.


🪜 Krok po kroku – Budowa planszy gry z Toolboxa


Krok 1: Otwórz 

MainWindow.xaml

  1. W Eksploratorze rozwiązań kliknij dwukrotnie MainWindow.xaml.
  2. Pojawi się okno z zakładką Projektant (góra) i XAML (dół).

👁 Jeśli nie widać projektu wizualnego – kliknij „Widok → Projektant” lub skrót Shift + F7.


Krok 2: Zamień domyślny Grid na Canvas

  1. Kliknij raz na puste pole projektu.
  2. W oknie Właściwości (z prawej) znajdź pole Typ kontenera.
  3. Jeśli to możliwe, wybierz Canvas – jeśli nie: 👉 Przejdź do kodu XAML i ręcznie zamień <Grid> na <Canvas Name=”Plansza”>.

Krok 3: Ustaw tło planszy

  1. Kliknij raz na Canvas (czyli tło).
  2. W oknie Właściwości znajdź opcję Background.
  3. Kliknij ikonę „…” obok.
  4. Wybierz Brush → ImageBrush
  5. Kliknij „…” przy ImageSource, wybierz Assets/tlogry.png.

📌 Obraz musi mieć ustawione wcześniej:

  • Akcja kompilacji: Zawartość
  • Kopiuj do katalogu wyjściowego: Zawsze kopiuj

Krok 4: Dodaj TextBlock z punktami

  1. Otwórz Toolbox (jeśli nie widać: Ctrl + Alt + X lub menu „Widok → Przybornik”).
  2. Przeciągnij TextBlock na pole gry.
  3. Kliknij na niego raz i ustaw w Właściwościach:
    • Name: LicznikPunktow
    • Text: Punkty: 0
    • FontSize: 24
    • FontWeight: Bold
    • Foreground: Biały
    • Canvas.Left: 10
    • Canvas.Top: 10

Krok 5: Dodaj TextBlock z życiami

  1. Przeciągnij drugi TextBlock.
  2. Ustaw:
    • Name: LicznikZyc
    • Text: Życia: 5
    • FontSize: 24
    • FontWeight: Bold
    • Foreground: #FFFA7C7C
    • Canvas.Left: 200
    • Canvas.Top: 10

Krok 6: Dodaj Image – wilk

  1. Przeciągnij z przybornika Image.
  2. Ustaw:
    • Name: Wilk
    • Width: 218
    • Height: 300
    • Source: kliknij „…” → wybierz Assets/wilkdol1.png
    • Canvas.Left: 246
    • Canvas.Top: 326

Krok 7: Dodaj  Border – koszyk

  1. Przeciągnij Border z Toolboxa.
  2. Ustaw:
    • Name: Koszyk
    • Width: 40
    • Height: 40
    • Opacity: 0.5
    • Background: Gray
    • Canvas.Left: 274
    • Canvas.Top: 452

Efekt końcowy:

Po kliknięciu Uruchom bez debugowania (Ctrl + F5) zobaczysz:
szary koszyk (który będzie odpowiadał za łapanie jajek).
pełne tło gry,
licznik punktów i żyć,
wilka na dole,

Twój kod xaml powinien wyglądać teraz tak.

<Window x:Class="JajkaWIlk.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="JajkaWilk" Height="750" Width="750"
        WindowStartupLocation="CenterScreen"
        ResizeMode="NoResize"
        SizeToContent="Manual"
        KeyDown="Window_KeyDown"
        KeyUp="Window_KeyUp">

    <Canvas Name="Plansza">

        <!-- Tło -->
        <Canvas.Background>
            <ImageBrush ImageSource="pack://application:,,,/Assets/tlogry.png" Stretch="Fill"/>
        </Canvas.Background>

        <!-- Punkty -->
        <TextBlock x:Name="LicznikPunktow"
                   FontSize="24"
                   FontWeight="Bold"
                   Foreground="White"
                   Canvas.Left="10"
                   Canvas.Top="10"
                   Text="Punkty: 0"/>

        <!-- Życia -->
        <TextBlock x:Name="LicznikZyc"
                   FontSize="24"
                   FontWeight="Bold"
                   Foreground="#FFFA7C7C"
                   Canvas.Left="200"
                   Canvas.Top="10"
                   Text="Życia: 5"/>

        <!-- Koszyk (obszar kolizji) -->
        <Border x:Name="Koszyk"
                Width="40"
                Height="40" 
                Opacity="0.5"
                Canvas.Left="274"
                Canvas.Top="452"/>

        <!-- Wilk -->
        <Image Name="Wilk"
               Width="218" Height="300"
               Source="pack://application:,,,/Assets/wilkdol1.png"
               Canvas.Left="246"
               Canvas.Top="326"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"/>

    </Canvas>
</Window>

Jeśli wszystko przebiegło pomyślnie, to właśnie stworzyłeś swoją pierwszą planszę w C# do gry. Gotowy na kolejny etap?


Etap3: Tworzenie logiki gry w pliku MainWindow.xaml.cs

W tym etapie zajmiemy się napisaniem kodu, który będzie obsługiwał całą logikę gry „Jajka Wilk”. Będziemy krok po kroku analizować i dodawać odpowiednie fragmenty kodu do pliku MainWindow.xaml.cs, wyjaśniając, co robi każda linia.


Krok 1: Import potrzebnych bibliotek

Na samej górze pliku dodajemy biblioteki, które umożliwią nam:

  • tworzenie elementów graficznych (Image, Canvas itp.),
  • obsługę zdarzeń (klawiatura),
  • tworzenie timerów do spadających jajek,
  • losowanie pozycji.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using System.Collections.Generic;

Krok 2: Deklaracja klasy głównej i elementów gry

Teraz zaczynamy pisać klasę MainWindow, która dziedziczy po klasie Window. W niej będą wszystkie nasze pola, metody i logika gry.

namespace JajkaWIlk
{
    public partial class MainWindow : Window
    {

Krok 3: Definiujemy typy pomocnicze – enum i struct

Chcemy mieć uporządkowane informacje o pozycjach wilka oraz o jajkach, dlatego tworzymy:

  • enum Pozycja – typ wyliczeniowy, który przechowuje możliwe pozycje wilka i jajek,
  • struct JajkoData – struktura przechowująca informacje o jednym jajku: jego grafikę i pozycję początkową.
        private enum Pozycja { LewaGora, LewyDol, PrawaGora, PrawyDol } // Pozycje grządek i wilka

        private struct JajkoData
        {
            public Image Obraz; // obraz jajka
            public Pozycja PozycjaStartowa; // z której grządki spada
        }

Krok 4: Pola klasy – zmienne przechowujące stan gry

Tworzymy zmienne, które będą potrzebne do działania gry:

  • generowanie liczb losowych,
  • zegary (timery),
  • lista jajek aktualnie spadających,
  • licznik punktów i żyć,
  • flagi dla wciśnięcia klawiszy,
  • aktualna pozycja wilka,
  • współrzędne startowe jajek (dopasowane do tła).
        private Random los = new Random();
        private DispatcherTimer timerNoweJajko = new DispatcherTimer();
        private DispatcherTimer timerRuchJajek = new DispatcherTimer();

        private List<JajkoData> aktywneJajka = new List<JajkoData>();

        private int punkty = 0;
        private int zycia = 5;

        private bool lewoWcisniete = false;
        private bool prawoWcisniete = false;
        private bool goraWcisnieta = false;
        private bool dolWcisniete = false;

        private Pozycja aktualnaPozycjaWilka = Pozycja.LewyDol;

        private readonly Point startLG = new Point(150, 140);
        private readonly Point startLD = new Point(100, 300);
        private readonly Point startPG = new Point(550, 135);
        private readonly Point startPD = new Point(550, 315);

Krok 5: Konstruktor – co ma się uruchomić na początku

Metoda MainWindow() to tzw. konstruktor. Jest wykonywana automatycznie po uruchomieniu gry. Dodajemy w niej:

  • inicjalizację komponentów (czyli załadowanie XAML-a),
  • podpięcie obsługi klawiszy (KeyDown, KeyUp),
  • konfigurację timerów do jajek (interwał i start),
  • inicjalizację liczników punktów i żyć.
        public MainWindow()
        {
            InitializeComponent(); // ładowanie wyglądu z XAML

            // obsługa klawiatury
            this.KeyDown += Window_KeyDown;
            this.KeyUp += Window_KeyUp;

            // start timera od jajek
            timerNoweJajko.Interval = TimeSpan.FromSeconds(los.Next(2, 5));
            timerNoweJajko.Tick += TimerNoweJajko_Tick;
            timerNoweJajko.Start();

            // start timera od ruchu jajek
            timerRuchJajek.Interval = TimeSpan.FromMilliseconds(70);
            timerRuchJajek.Tick += TimerRuchJajek_Tick;
            timerRuchJajek.Start();

            AktualizujPunkty(); // licznik punktów
            AktualizujZycia();  // licznik żyć
        }

Krok 6: Tworzenie nowych jajek

Timer timerNoweJajko wywołuje co kilka sekund metodę TimerNoweJajko_Tick, która tworzy nowe jajko w losowym miejscu.

        private void TimerNoweJajko_Tick(object? sender, EventArgs e)
        {
            DodajNoweJajko(); // dodaj nowe jajko do gry
            timerNoweJajko.Interval = TimeSpan.FromSeconds(los.Next(2, 5)); // ustaw kolejny czas
        }

Metoda DodajNoweJajko() ustala pozycję startową, tworzy obrazek jajka i dodaje go do planszy i listy.

        private void DodajNoweJajko()
        {
            int wybor = los.Next(4); // losuj jedną z czterech pozycji
            Pozycja pozycja = (Pozycja)wybor; // przekształć na wartość typu Pozycja

            Point start = pozycja switch
            {
                Pozycja.LewaGora => startLG,
                Pozycja.LewyDol => startLD,
                Pozycja.PrawaGora => startPG,
                Pozycja.PrawyDol => startPD,
                _ => startLG
            };

            Image jajko = new Image
            {
                Width = 50,
                Height = 50,
                Source = new BitmapImage(new Uri("pack://application:,,,/Assets/egg1.png")) // grafika jajka
            };

            Canvas.SetLeft(jajko, start.X); // pozycja na planszy X
            Canvas.SetTop(jajko, start.Y);  // pozycja na planszy Y
            Plansza.Children.Add(jajko); // dodaj do planszy

            aktywneJajka.Add(new JajkoData { Obraz = jajko, PozycjaStartowa = pozycja }); // zapisz do listy
        }

Krok 7: Poruszanie jajkami i wykrywanie kolizji

W tej części gry zajmujemy się ruchem wszystkich jajek na planszy. Timer timerRuchJajek uruchamia się co 70 ms i przesuwa każde jajko w odpowiednim kierunku. Dodatkowo sprawdza, czy jajko:

  • zostało złapane przez gracza (kolizja z koszykiem),
  • spadło poza planszę (utrata życia).

To najważniejsza metoda logiki gry, odpowiadająca za obsługę całej mechaniki łapania i tracenia jajek.

        private void TimerRuchJajek_Tick(object? sender, EventArgs e)
        {
            List<JajkoData> doUsuniecia = new List<JajkoData>(); // lista jajek do usunięcia

            foreach (var jajkoData in aktywneJajka)
            {
                double x = Canvas.GetLeft(jajkoData.Obraz);
                double y = Canvas.GetTop(jajkoData.Obraz);

                // Ruch jajek zależny od pozycji startowej (trajektoria)
                switch (jajkoData.PozycjaStartowa)
                {
                    case Pozycja.LewaGora: x += 3.5; y += 1.6; break;
                    case Pozycja.LewyDol: x += 3.5; y += 1.9; break;
                    case Pozycja.PrawaGora: x -= 3.5; y += 1.7; break;
                    case Pozycja.PrawyDol: x -= 3.5; y += 1.9; break;
                }

                // Ustawiamy nową pozycję jajka
                Canvas.SetLeft(jajkoData.Obraz, x);
                Canvas.SetTop(jajkoData.Obraz, y);

                // Tworzymy prostokąty do sprawdzenia kolizji (jajko i koszyk)
                Rect jajkoRect = new Rect(x, y, jajkoData.Obraz.Width, jajkoData.Obraz.Height);
                Rect koszykRect = new Rect(Canvas.GetLeft(Koszyk), Canvas.GetTop(Koszyk), Koszyk.Width, Koszyk.Height);

                // Czy jajko zostało złapane?
                if (jajkoRect.IntersectsWith(koszykRect) && jajkoData.PozycjaStartowa == aktualnaPozycjaWilka)
                {
                    Plansza.Children.Remove(jajkoData.Obraz); // usuń z planszy
                    doUsuniecia.Add(jajkoData); // oznacz do usunięcia z listy
                    punkty++; // dodaj punkt
                    AktualizujPunkty();
                }
                else if (y > 700 || x < 0 || x > 700) // Czy jajko spadło poza planszę?
                {
                    Plansza.Children.Remove(jajkoData.Obraz);
                    doUsuniecia.Add(jajkoData);
                    zycia--; // tracimy życie
                    AktualizujZycia();

                    // Koniec gry, jeśli brak żyć
                    if (zycia <= 0)
                    {
                        timerNoweJajko.Stop();
                        timerRuchJajek.Stop();
                        MessageBox.Show("Koniec gry! Twoje punkty: " + punkty);
                    }
                }
            }

            // Usuń złapane lub rozbite jajka z listy
            foreach (var jajko in doUsuniecia)
                aktywneJajka.Remove(jajko);
        }

Krok 8: Aktualizacja liczników – punkty i życia

Aby gracz wiedział, ile ma punktów i żyć, musimy zaktualizować odpowiednie pola tekstowe na planszy. Zrobimy to w dwóch metodach: AktualizujPunkty() i AktualizujZycia().

Punkty

Wyświetlamy aktualną liczbę punktów w kontrolce LicznikPunktow, która znajduje się na planszy (dodana wcześniej w pliku XAML).

private void AktualizujPunkty()
{
    LicznikPunktow.Text = $"Punkty: {punkty}"; // aktualizacja napisu z punktami
}

Życia

Sprawdzamy, czy TextBlock LicznikZyc istnieje. Jeśli tak – aktualizujemy jego tekst. Jeśli nie – tworzymy go na nowo (przy starcie gry).

private void AktualizujZycia()
{
    if (Plansza.FindName("LicznikZyc") is TextBlock t)
    {
        // jeśli licznik istnieje, zmień tylko wartość
        t.Text = $"\u2665: {zycia}";
    }
    else
    {
        // jeśli nie istnieje – utwórz licznik żyć
        TextBlock zyciaBlock = new TextBlock
        {
            Name = "LicznikZyc",
            FontSize = 24,
            FontWeight = FontWeights.Bold,
            Foreground = Brushes.Red,
            Text = $"\u2665: {zycia}"
        };
        Canvas.SetLeft(zyciaBlock, 650);
        Canvas.SetTop(zyciaBlock, 10);
        Plansza.Children.Add(zyciaBlock);
        RegisterName("LicznikZyc", zyciaBlock); // rejestrujemy nowy TextBlock po imieniu
    }
}

Krok 9: Sterowanie klawiaturą – zmiana pozycji wilka

Gracz steruje wilkiem za pomocą klawiszy strzałek: lewo/prawo + góra/dół. Musimy wykryć, które klawisze są aktualnie wciśnięte i odpowiednio ustawić pozycję wilka oraz koszyka.

Wciśnięcie klawisza (KeyDown)

Tutaj ustawiamy flagi (bo możliwe są kombinacje klawiszy) i na ich podstawie ustalamy pozycję wilka oraz koszyka.

private void Window_KeyDown(object sender, KeyEventArgs e)
{
    // zapamiętujemy które klawisze są wciśnięte
    if (e.Key == Key.Left) lewoWcisniete = true;
    if (e.Key == Key.Right) prawoWcisniete = true;
    if (e.Key == Key.Up) goraWcisnieta = true;
    if (e.Key == Key.Down) dolWcisniete = true;

    // kombinacje sterujące wilkiem
    if (lewoWcisniete && goraWcisnieta)
    {
        aktualnaPozycjaWilka = Pozycja.LewaGora;
        UstawWilka(240, 180, "Assets/wilkgora1.png", false);
        UstawKoszyk(274, 238);
    }
    else if (lewoWcisniete && dolWcisniete)
    {
        aktualnaPozycjaWilka = Pozycja.LewyDol;
        UstawWilka(240, 330, "Assets/wilkdol1.png", false);
        UstawKoszyk(274, 432);
    }
    else if (prawoWcisniete && goraWcisnieta)
    {
        aktualnaPozycjaWilka = Pozycja.PrawaGora;
        UstawWilka(240, 195, "Assets/wilkgora1.png", true); // prawa strona = obrócenie wilka
        UstawKoszyk(385, 228);
    }
    else if (prawoWcisniete && dolWcisniete)
    {
        aktualnaPozycjaWilka = Pozycja.PrawyDol;
        UstawWilka(240, 330, "Assets/wilkdol1.png", true);
        UstawKoszyk(385, 432);
    }

    e.Handled = true; // zatrzymujemy dalsze przetwarzanie klawisza
}

Zwolnienie klawisza (KeyUp)

Gdy gracz puszcza klawisz, flaga powinna wrócić do wartości false.

private void Window_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Left) lewoWcisniete = false;
    if (e.Key == Key.Right) prawoWcisniete = false;
    if (e.Key == Key.Up) goraWcisnieta = false;
    if (e.Key == Key.Down) dolWcisniete = false;
}

Krok 10: Pomocnicze metody do ustawiania wilka i koszyka

Te metody są wywoływane w zależności od pozycji wilka. Dzięki nim możemy:

  • zmienić obrazek wilka,
  • ustawić go we właściwe miejsce na planszy,
  • przesunąć przezroczysty koszyk do odpowiedniej pozycji (służy do sprawdzania kolizji z jajkami).

Ustaw Wilka

private void UstawWilka(double left, double top, string sciezka, bool obrocWPrawo)
{
    // ładowanie nowego obrazka wilka
    var bitmap = new BitmapImage();
    bitmap.BeginInit();
    bitmap.UriSource = new Uri($"pack://application:,,,/{sciezka}", UriKind.Absolute);
    bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
    bitmap.EndInit();

    Wilk.Source = bitmap; // ustawiamy nowy obrazek
    Canvas.SetLeft(Wilk, left); // przesuwamy na planszy
    Canvas.SetTop(Wilk, top);

    // jeśli prawa strona – odbij obrazek w poziomie
    Wilk.LayoutTransform = obrocWPrawo ? new ScaleTransform(-1, 1) : null;
}

Ustaw Koszyk

private void UstawKoszyk(double left, double top)
{
    // ustawiamy przezroczysty koszyk do kolizji z jajkiem
    Canvas.SetLeft(Koszyk, left);
    Canvas.SetTop(Koszyk, top);
}

Cały kod dla CS

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using System.Collections.Generic;

namespace JajkaWIlk
{
    public partial class MainWindow : Window
    {
        // ========== ENUM i STRUKTURY ==========

        // Pozycje, w których może znajdować się wilk (i z których mogą spadać jajka)

        private enum Pozycja { LewaGora, LewyDol, PrawaGora, PrawyDol }

        // Dane o jajku: jego obrazek i pozycja startowa

        private struct JajkoData
        {
            public Image Obraz;
            public Pozycja PozycjaStartowa;
        }

        // ========== POLA KLASOWE (czyli zmienne dla całej gry) ==========

        private Random los = new Random(); // Losowanie (np. wyboru pozycji jajka)

        private DispatcherTimer timerNoweJajko = new DispatcherTimer(); // Zegar do tworzenia nowych jajek

        private DispatcherTimer timerRuchJajek = new DispatcherTimer(); // Zegar do poruszania jajkami

        private List<JajkoData> aktywneJajka = new List<JajkoData>(); // Lista aktualnie spadających jajek

        private int punkty = 0; // Liczba zdobytych punktów
        private int zycia = 5;  // Liczba pozostałych żyć

        // Flagi wciskania klawiszy (pozwalają sprawdzić, które klawisze są aktualnie wciśnięte)

        private bool lewoWcisniete = false;
        private bool prawoWcisniete = false;
        private bool goraWcisnieta = false;
        private bool dolWcisniete = false;

        // Aktualna pozycja wilka

        private Pozycja aktualnaPozycjaWilka = Pozycja.LewyDol;

        // Pozycje startowe jajek (dopasowane do grządek na obrazku)

        private readonly Point startLG = new Point(150, 140);
        private readonly Point startLD = new Point(100, 300);
        private readonly Point startPG = new Point(550, 135);
        private readonly Point startPD = new Point(550, 315);

        // ========== KONSTRUKTOR ==========

        public MainWindow()
        {
            InitializeComponent();

            // Obsługa klawiatury

            this.KeyDown += Window_KeyDown;
            this.KeyUp += Window_KeyUp;

            // Start zegara do generowania jajek

            timerNoweJajko.Interval = TimeSpan.FromSeconds(los.Next(2, 5)); // Tu można zmienić  
                                    prędkość pojawiania się jajek
            timerNoweJajko.Tick += TimerNoweJajko_Tick;
            timerNoweJajko.Start();

                                   // Start zegara do ruchu jajek
            timerRuchJajek.Interval = TimeSpan.FromMilliseconds(70);    // Tu można zmienić szybkość 
                                  opadania jajek
            timerRuchJajek.Tick += TimerRuchJajek_Tick;
            timerRuchJajek.Start();

            AktualizujPunkty(); // Ustawienie tekstu na starcie
            AktualizujZycia();  // Ustawienie liczby żyć na 
                                   starcie
        }

        // ========== TWORZENIE JAJEK ==========

        private void TimerNoweJajko_Tick(object? sender, EventArgs e)
        {
            DodajNoweJajko();     // Dodaje nowe jajko na planszę

            timerNoweJajko.Interval = TimeSpan.FromSeconds(los.Next(2, 5)); // Ustala czas do 
                                          następnego jajka
        }

        private void DodajNoweJajko()
        {
            int wybor = los.Next(4); // Losuje jedną z 4 pozycji
            Pozycja pozycja = (Pozycja)wybor;

                                     // Ustala punkt startowy 
                                    jajka w zależności od pozycji

            Point start = pozycja switch
            {
                Pozycja.LewaGora => startLG,
                Pozycja.LewyDol => startLD,
                Pozycja.PrawaGora => startPG,
                Pozycja.PrawyDol => startPD,
                _ => startLG
            };

            // Tworzy nowe jajko jako obrazek

            Image jajko = new Image
            {
                Width = 50,
                Height = 50,
                Source = new BitmapImage(new Uri("pack://application:,,,/Assets/egg1.png")) // Tutaj można 
                                          podmienić grafikę jajka
            };

                            // Ustawienie pozycji startowej jajka

            Canvas.SetLeft(jajko, start.X);
            Canvas.SetTop(jajko, start.Y);
            Plansza.Children.Add(jajko);

                           // Dodanie do listy aktywnych jajek

            aktywneJajka.Add(new JajkoData { Obraz = jajko, PozycjaStartowa = pozycja });
        }

        // ========== RUCH I KOLIZJE JAJEK ==========

        private void TimerRuchJajek_Tick(object? sender, EventArgs e)
        {
            List<JajkoData> doUsuniecia = new List<JajkoData>();

            foreach (var jajkoData in aktywneJajka)
            {
                double x = Canvas.GetLeft(jajkoData.Obraz);
                double y = Canvas.GetTop(jajkoData.Obraz);

                // Tu można zmieniać trajektorie spadania jajek (zmieniając współczynniki x i y)

                switch (jajkoData.PozycjaStartowa)
                {
                    case Pozycja.LewaGora: x += 3.5; y += 1.6; break;
                    case Pozycja.LewyDol: x += 3.5; y += 1.9; break;
                    case Pozycja.PrawaGora: x -= 3.5; y += 1.7; break;
                    case Pozycja.PrawyDol: x -= 3.5; y += 1.9; break;
                }

                // Ustawienie nowej pozycji jajka

                Canvas.SetLeft(jajkoData.Obraz, x);
                Canvas.SetTop(jajkoData.Obraz, y);

                // Sprawdzenie kolizji jajka z koszykiem

                Rect jajkoRect = new Rect(x, y, jajkoData.Obraz.Width, jajkoData.Obraz.Height);
                Rect koszykRect = new Rect(Canvas.GetLeft(Koszyk), Canvas.GetTop(Koszyk), Koszyk.Width, Koszyk.Height);

                // Jeśli jajko zostało złapane

                if (jajkoRect.IntersectsWith(koszykRect) && jajkoData.PozycjaStartowa == aktualnaPozycjaWilka)
                {
                    Plansza.Children.Remove(jajkoData.Obraz);
                    doUsuniecia.Add(jajkoData);
                    punkty++;
                    AktualizujPunkty();
                }
                    // Jeśli jajko spadło poza ekran

                else if (y > 700 || x < 0 || x > 700)
                {
                    Plansza.Children.Remove(jajkoData.Obraz);
                    doUsuniecia.Add(jajkoData);
                    zycia--;
                    AktualizujZycia();

                    // Jeśli gracz przegrał wszystkie życia

                    if (zycia <= 0)
                    {
                        timerNoweJajko.Stop();
                        timerRuchJajek.Stop();
                        MessageBox.Show("Koniec gry! Twoje punkty: " + punkty);
                    }
                }
            }

                     // Usunięcie jajek z listy aktywnych

            foreach (var jajko in doUsuniecia)
                aktywneJajka.Remove(jajko);
        }

        // ========== AKTUALIZACJA PUNKTÓW I ŻYĆ ==========

        private void AktualizujPunkty()
        {
            LicznikPunktow.Text = $"Punkty: {punkty}";
        }

        private void AktualizujZycia()
        {
            if (Plansza.FindName("LicznikZyc") is TextBlock t)
            {
                t.Text = $"\u2665: {zycia}";
            }
            else
            {
                // Jeśli licznik nie istnieje (np. przy starcie), 
                   tworzy go

                TextBlock zyciaBlock = new TextBlock
                {
                    Name = "LicznikZyc",
                    FontSize = 24,
                    FontWeight = FontWeights.Bold,
                    Foreground = Brushes.Red,
                    Text = $"\u2665: {zycia}"
                };
                Canvas.SetLeft(zyciaBlock, 650);
                Canvas.SetTop(zyciaBlock, 10);
                Plansza.Children.Add(zyciaBlock);
                RegisterName("LicznikZyc", zyciaBlock);
            }
        }

        // ========== OBSŁUGA KLAWISZY (STEROWANIE WILKIEM) ==========

        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            // Zaznaczamy, które klawisze są wciśnięte

            if (e.Key == Key.Left) lewoWcisniete = true;
            if (e.Key == Key.Right) prawoWcisniete = true;
            if (e.Key == Key.Up) goraWcisnieta = true;
            if (e.Key == Key.Down) dolWcisniete = true;

            // Kombinacje klawiszy → zmiana pozycji wilka

            if (lewoWcisniete && goraWcisnieta)
            {
                aktualnaPozycjaWilka = Pozycja.LewaGora;
                UstawWilka(240, 180, "Assets/wilkgora1.png", false);
                UstawKoszyk(274, 238);
            }
            else if (lewoWcisniete && dolWcisniete)
            {
                aktualnaPozycjaWilka = Pozycja.LewyDol;
                UstawWilka(240, 330, "Assets/wilkdol1.png", false);
                UstawKoszyk(274, 432);
            }
            else if (prawoWcisniete && goraWcisnieta)
            {
                aktualnaPozycjaWilka = Pozycja.PrawaGora;
                UstawWilka(240, 195, "Assets/wilkgora1.png", true);
                UstawKoszyk(385, 228);
            }
            else if (prawoWcisniete && dolWcisniete)
            {
                aktualnaPozycjaWilka = Pozycja.PrawyDol;
                UstawWilka(240, 330, "Assets/wilkdol1.png", true);
                UstawKoszyk(385, 432);
            }

            e.Handled = true; // Zatrzymanie dalszej obsługi 
                                 klawisza
        }

        private void Window_KeyUp(object sender, KeyEventArgs e)
        {
            // Gdy klawisz został puszczony – flaga wraca na 
               false
            if (e.Key == Key.Left) lewoWcisniete = false;
            if (e.Key == Key.Right) prawoWcisniete = false;
            if (e.Key == Key.Up) goraWcisnieta = false;
            if (e.Key == Key.Down) dolWcisniete = false;
        }

        // ========== ZMIANA GRAFIKI WILKA I POZYCJI KOSZYKA ==========

        private void UstawWilka(double left, double top, string sciezka, bool obrocWPrawo)
        {
            // Ładuje nową grafikę wilka z pliku

            var bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.UriSource = new Uri($"pack://application:,,,/{sciezka}", UriKind.Absolute);
            bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
            bitmap.EndInit();

            // Ustawia nowy obrazek i pozycję wilka

            Wilk.Source = bitmap;
            Canvas.SetLeft(Wilk, left);
            Canvas.SetTop(Wilk, top);

            // Jeśli obrazek ma być odbity (dla prawej strony)

            Wilk.LayoutTransform = obrocWPrawo ? new ScaleTransform(-1, 1) : null;
        }

        private void UstawKoszyk(double left, double top)
        {
            // Ustawienie pozycji niewidzialnego koszyka (do kolizji z jajkami)

            Canvas.SetLeft(Koszyk, left);
            Canvas.SetTop(Koszyk, top);
        }
    }
}