diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6794817
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.venv/
+__pycache__/
+*.pyc
+.idea/
+.vscode/
diff --git a/README.md b/README.md
index e69de29..ff1e944 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,87 @@
+# OOP w Pythonie przez bibliotekę arcade — kurs 5-dniowy
+
+Kurs dla osoby, która dobrze zna proceduralnego Pythona (pętle, funkcje, listy,
+słowniki, algorytmy), ale nie pisała jeszcze klas. Przez pięć dni budujemy gry
+w bibliotece [arcade](https://api.arcade.academy/) i przy okazji poznajemy
+obiektowość: klasy, konstruktory, dziedziczenie, nadpisywanie metod, `property`.
+
+Uwaga: kurs korzysta z **arcade 3.x** (zweryfikowano na 3.3.3). API 3.x różni się
+od starych tutoriali z wersji 2.x — patrz sekcja „Pułapki API” na dole.
+
+## Setup
+
+```bash
+# 1. Wirtualne środowisko (w katalogu projektu)
+python3 -m venv .venv
+
+# 2. Aktywacja
+source .venv/bin/activate # Linux / macOS
+# .venv\Scripts\activate # Windows (PowerShell / cmd)
+
+# 3. Instalacja zależności
+pip install -r requirements.txt
+
+# 4. Sprawdzenie, czy działa
+python -c "import arcade; print(arcade.VERSION)"
+python dzien1/01_okno.py
+```
+
+Jeśli po `python dzien1/01_okno.py` otworzy się okno z napisem — środowisko jest
+gotowe. Zamykamy okno krzyżykiem albo `Esc` (tam, gdzie jest obsłużony).
+
+## Plan dni
+
+| Katalog | Dzień | Temat | Co ćwiczymy z OOP |
+| ------------------------ | ----- | ------------------------------------------------- | ------------------------------------------------------------ |
+| `dzien1/` | 1 | Pierwsze okno, rysowanie, ruch sprite'a | klasa, `__init__`, `super()`, dziedziczenie po `arcade.Window`, nadpisywanie metod |
+| `dzien2/` | 2 | Zbieranie monet + własna hierarchia wrogów | klasa bazowa i klasy pochodne, nadpisywanie `update()`, polimorfizm |
+| `dzien3_4_platformowka/` | 3–4 | Platformówka wg oficjalnego tutoriala | kompozycja obiektów (silnik fizyki, kamera, sceny), `property` z walidacją |
+| `dzien5_projekt/` | 5 | Własna gra od zera | projektowanie klas, podział na pliki, `arcade.View` jako stany gry |
+| `assets/` | — | Grafika i dźwięki (do pobrania samodzielnie) | — |
+
+## Zasady pracy
+
+1. **Uzupełniaj `TODO`.** Każdy plik startowy *działa* od razu — uruchom go
+ najpierw bez zmian, zobacz co robi, dopiero potem szukaj `TODO:` i dopisuj kod.
+2. **Uruchamiaj często.** Po każdej małej zmianie (5–15 linii) odpal program.
+ Łatwiej znaleźć błąd w pięciu nowych liniach niż w stu.
+3. **Commit po każdym działającym kroku.** Nie po każdym dniu — po każdym
+ momencie, w którym gra znowu się uruchamia:
+ ```bash
+ git add -A
+ git commit -m "dzien2: klasa Enemy odbija się od krawędzi"
+ ```
+ Dzięki temu zawsze można wrócić do ostatniej wersji, która działała.
+4. **Czytaj komunikaty błędów od dołu.** Ostatnia linia traceback mówi *co* się
+ stało, linie wyżej mówią *gdzie*.
+5. **Nie kopiuj kodu, którego nie rozumiesz.** Jeśli coś w przykładzie jest
+ niejasne — zmień to i zobacz, co się zepsuje. To najszybszy sposób nauki.
+
+## Materiały
+
+- Primer (szybkie wprowadzenie do arcade):
+- Kurs „Learn Arcade” (długi, po angielsku, z zadaniami):
+- Tutorial platformówki (podstawa dni 3–4):
+- Przykłady kodu (kopalnia gotowych rozwiązań):
+- Darmowe assety (grafika, dźwięki):
+
+## Pułapki API — arcade 3.x kontra stare tutoriale
+
+Duża część poradników w internecie opisuje arcade 2.x. Najczęstsze różnice:
+
+| Stare (2.x) | Aktualne (3.x) |
+| -------------------------------------- | ----------------------------------------------------------- |
+| `arcade.start_render()` w `on_draw` | `self.clear()` |
+| `sprite_list.on_update(delta_time)` | `sprite_list.update(delta_time)` — `SpriteList` **nie ma** `on_update` |
+| `def update(self):` w klasie sprite'a | `def update(self, delta_time=1/60, *args, **kwargs):` |
+| `arcade.draw_rectangle_filled(...)` | `arcade.draw_lrbt_rectangle_filled(...)` lub `draw_lbwh_rectangle_filled(...)` |
+| `arcade.set_background_color(...)` | `self.background_color = ...` (atrybut okna / widoku) |
+| `arcade.Camera(...)` | `arcade.Camera2D(...)` |
+
+Jeżeli tutorial nie działa — najpierw sprawdź tę tabelę, potem oficjalną
+dokumentację dla 3.x.
+
+Ostrzeżenie `PerformanceWarning: draw_text is an extremely slow function`
+pojawia się normalnie i **nie jest błędem** — arcade sugeruje, żeby przy wielu
+napisach tworzyć obiekty `arcade.Text` raz w `__init__` i tylko je rysować.
+Przy kilku napisach na ekranie można je zignorować.
diff --git a/assets/README.md b/assets/README.md
new file mode 100644
index 0000000..69d29e3
--- /dev/null
+++ b/assets/README.md
@@ -0,0 +1 @@
+Grafikę i dźwięki do gier pobierz z (darmowe, licencja CC0) i wypakuj tutaj.
diff --git a/dzien1/01_okno.py b/dzien1/01_okno.py
new file mode 100644
index 0000000..be7846f
--- /dev/null
+++ b/dzien1/01_okno.py
@@ -0,0 +1,123 @@
+"""
+Dzień 1, krok 1 — pierwsze okno.
+
+Cel: zobaczyć, że "klasa" to nic strasznego — to przepis na obiekt, który
+biblioteka arcade sama za nas uruchomi i sama będzie wołać jego metody.
+
+Uruchom:
+ python dzien1/01_okno.py
+"""
+
+import arcade
+
+# Stałe piszemy WIELKIMI literami. To zwykłe zmienne globalne — po to,
+# żeby rozmiar okna zmieniać w jednym miejscu, a nie w pięciu.
+SZEROKOSC = 800
+WYSOKOSC = 600
+TYTUL = "Dzień 1 — moje pierwsze okno"
+
+
+# ----------------------------------------------------------------------------
+# KLASA = przepis na obiekt.
+#
+# Zapis `class MyGame(arcade.Window)` znaczy: "MyGame jest rodzajem
+# arcade.Window". To DZIEDZICZENIE. Nasza klasa dostaje za darmo wszystko,
+# co potrafi okno z biblioteki: otwiera się, reaguje na mysz, na klawiaturę,
+# odświeża się 60 razy na sekundę. My dopisujemy tylko to, co ma być NASZE.
+#
+# arcade.Window nazywamy klasą BAZOWĄ (albo rodzicem/nadklasą),
+# a MyGame — klasą POCHODNĄ (albo dzieckiem/podklasą).
+# ----------------------------------------------------------------------------
+class MyGame(arcade.Window):
+ """Okno naszej gry."""
+
+ def __init__(self):
+ """
+ KONSTRUKTOR — metoda wołana automatycznie w momencie tworzenia
+ obiektu, czyli gdy niżej piszemy `MyGame()`.
+
+ `self` to sam tworzony obiekt. Każda metoda w klasie dostaje go jako
+ pierwszy argument — dzięki temu wie, na czyich danych pracuje.
+ W wywołaniu `self` się NIE podaje: piszemy `okno.on_draw()`,
+ a Python sam podstawia `okno` pod `self`.
+ """
+ # super() to "mój rodzic", czyli arcade.Window.
+ # Tu mówimy: "rodzicu, wykonaj swój konstruktor z takimi parametrami".
+ # Bez tej linii okno w ogóle by się nie utworzyło — to rodzic zna
+ # wszystkie szczegóły otwierania okna w systemie.
+ super().__init__(SZEROKOSC, WYSOKOSC, TYTUL)
+
+ # Atrybuty obiektu — zmienne, które "żyją" wewnątrz obiektu i są
+ # widoczne we WSZYSTKICH metodach tej klasy (przez self.).
+ # W kodzie proceduralnym trzymalibyśmy je w zmiennych globalnych.
+ self.background_color = arcade.color.DARK_BLUE
+ self.licznik_klatek = 0
+
+ def on_draw(self):
+ """
+ NADPISANIE METODY: arcade.Window ma już metodę on_draw(), ale pustą.
+ Definiując ją u siebie, podmieniamy tamtą wersję na własną.
+
+ Tej metody NIGDY nie wołamy sami — arcade woła ją za nas przy każdej
+ klatce (~60 razy na sekundę). To jest tzw. odwrócenie sterowania:
+ my nie piszemy pętli głównej, my tylko wypełniamy gotowe miejsca.
+ """
+ # W arcade 3.x czyścimy ekran przez self.clear().
+ # (W starych tutorialach do 2.x było arcade.start_render() — to inna
+ # funkcja o zupełnie innym znaczeniu, nie używaj jej tutaj.)
+ self.clear()
+
+ arcade.draw_text(
+ "Cześć! To jest obiekt klasy MyGame.",
+ x=50,
+ y=WYSOKOSC / 2,
+ color=arcade.color.WHITE,
+ font_size=22,
+ )
+ arcade.draw_text(
+ f"Klatka nr {self.licznik_klatek}",
+ x=50,
+ y=WYSOKOSC / 2 - 40,
+ color=arcade.color.GOLD,
+ font_size=16,
+ )
+
+ # TODO 1: dorysuj koło na środku ekranu.
+ # arcade.draw_circle_filled(środek_x, środek_y, promień, kolor)
+ # np. arcade.draw_circle_filled(SZEROKOSC / 2, 120, 40, arcade.color.RED)
+
+ # TODO 2: dorysuj prostokąt (np. "podłogę" na dole ekranu).
+ # W arcade 3.x są dwie wygodne wersje:
+ # arcade.draw_lrbt_rectangle_filled(left, right, bottom, top, kolor)
+ # arcade.draw_lbwh_rectangle_filled(left, bottom, width, height, kolor)
+ # Wybierz jedną i narysuj pas o wysokości 40 pikseli przy dolnej krawędzi.
+
+ def on_update(self, delta_time: float):
+ """
+ Druga metoda wołana automatycznie ~60 razy na sekundę — tu zmieniamy
+ stan gry (a w on_draw tylko rysujemy to, co już wyliczone).
+
+ delta_time to czas w sekundach, jaki minął od poprzedniej klatki
+ (zwykle ok. 0.016 s). Przyda się w dniu 2, żeby ruch był równy
+ niezależnie od szybkości komputera.
+ """
+ self.licznik_klatek += 1
+
+ def on_key_press(self, symbol: int, modifiers: int):
+ """Kolejna nadpisana metoda — arcade woła ją po wciśnięciu klawisza."""
+ if symbol == arcade.key.ESCAPE:
+ self.close()
+
+
+def main():
+ # TU powstaje obiekt: MyGame() uruchamia __init__ i zwraca gotowe okno.
+ # Zmienna `okno` to jedna konkretna INSTANCJA klasy MyGame.
+ okno = MyGame()
+
+ # arcade.run() startuje pętlę główną: w kółko woła on_update() i on_draw()
+ # naszego obiektu, aż okno zostanie zamknięte.
+ arcade.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dzien1/02_sprite_ruch.py b/dzien1/02_sprite_ruch.py
new file mode 100644
index 0000000..b8cac25
--- /dev/null
+++ b/dzien1/02_sprite_ruch.py
@@ -0,0 +1,135 @@
+"""
+Dzień 1, krok 2 — sprite i sterowanie z klawiatury.
+
+Nowe pojęcia:
+ * sprite — obiekt gry, który ma pozycję, wygląd i prędkość
+ * SpriteList — pojemnik na sprite'y; rysuje i aktualizuje je wszystkie naraz
+ * change_x / change_y — prędkość sprite'a (o ile pikseli przesuwa się w klatce)
+
+Uruchom:
+ python dzien1/02_sprite_ruch.py
+
+Sterowanie: strzałki. Wyjście: Esc.
+"""
+
+import arcade
+
+SZEROKOSC = 800
+WYSOKOSC = 600
+TYTUL = "Dzień 1 — ruch sprite'a"
+
+PROMIEN_GRACZA = 25
+PREDKOSC_GRACZA = 5 # w pikselach na klatkę (arcade przelicza to przez delta_time)
+
+
+class MyGame(arcade.Window):
+ """Okno gry — znowu dziedziczymy po arcade.Window."""
+
+ def __init__(self):
+ super().__init__(SZEROKOSC, WYSOKOSC, TYTUL)
+ self.background_color = arcade.color.DARK_SLATE_GRAY
+
+ # Atrybuty deklarujemy w __init__, nawet jeśli na razie są puste.
+ # Dzięki temu z jednego miejsca widać, jakie dane ma obiekt.
+ self.player_list = arcade.SpriteList()
+ self.player = None
+
+ self.setup()
+
+ def setup(self):
+ """
+ Metoda pomocnicza — nie jest częścią arcade, sami ją wymyśliliśmy.
+ Ustawia grę w stan początkowy. Trzymamy to osobno od __init__, bo
+ później (np. po przegranej) będziemy chcieli zrestartować grę bez
+ tworzenia nowego okna — wystarczy zawołać setup() jeszcze raz.
+ """
+ self.player_list = arcade.SpriteList()
+
+ # SpriteCircle to gotowy sprite w kształcie koła — klasa z biblioteki.
+ # Tworzymy jej instancję: SpriteCircle(promień, kolor).
+ self.player = arcade.SpriteCircle(PROMIEN_GRACZA, arcade.color.AMAZON)
+
+ # center_x / center_y to atrybuty każdego sprite'a — jego pozycja.
+ self.player.center_x = SZEROKOSC / 2
+ self.player.center_y = WYSOKOSC / 2
+
+ # Wkładamy sprite'a do listy. Od teraz wystarczy operować na liście.
+ self.player_list.append(self.player)
+
+ def on_draw(self):
+ self.clear()
+
+ # SpriteList sam rysuje wszystkie swoje sprite'y — jedno wywołanie.
+ self.player_list.draw()
+
+ arcade.draw_text(
+ "Strzałki — ruch, Esc — wyjście",
+ x=10,
+ y=10,
+ color=arcade.color.WHITE,
+ font_size=14,
+ )
+
+ def on_update(self, delta_time: float):
+ """
+ UWAGA na API arcade 3.x:
+ SpriteList NIE MA metody on_update(). Listę odświeżamy metodą
+ update(delta_time), a ona woła update() na każdym sprite'cie.
+
+ Domyślne Sprite.update() dodaje change_x/change_y do pozycji — czyli
+ samo przesuwanie mamy za darmo. W dniu 2 nadpiszemy update() we
+ własnej klasie, żeby sprite robił coś więcej.
+ """
+ self.player_list.update(delta_time)
+
+ # TODO 1: nie pozwól graczowi wyjechać poza okno.
+ # Każdy sprite ma atrybuty left, right, bottom, top (krawędzie).
+ # Przypisanie do nich przesuwa sprite'a, np.:
+ # if self.player.left < 0:
+ # self.player.left = 0
+ # Dopisz analogiczne warunki dla right (> SZEROKOSC),
+ # bottom (< 0) i top (> WYSOKOSC).
+
+ def on_key_press(self, symbol: int, modifiers: int):
+ """
+ Wciśnięcie klawisza USTAWIA prędkość — nie przesuwa gracza od razu.
+ Samo przesuwanie dzieje się w on_update, klatka po klatce.
+ Dzięki temu ruch jest płynny, a nie skokowy.
+ """
+ if symbol == arcade.key.LEFT:
+ self.player.change_x = -PREDKOSC_GRACZA
+ elif symbol == arcade.key.RIGHT:
+ self.player.change_x = PREDKOSC_GRACZA
+ elif symbol == arcade.key.UP:
+ self.player.change_y = PREDKOSC_GRACZA
+ elif symbol == arcade.key.DOWN:
+ self.player.change_y = -PREDKOSC_GRACZA
+ elif symbol == arcade.key.ESCAPE:
+ self.close()
+
+ # TODO 2: dodaj obsługę klawisza SPACE (arcade.key.SPACE).
+ # Wymyśl efekt i zapisz go w atrybucie obiektu, np.:
+ # - zmiana koloru gracza: self.player.color = arcade.color.RED
+ # - "dopalacz": ustaw self.szybki = True i w on_key_press mnóż
+ # PREDKOSC_GRACZA razy 2 (pamiętaj o wyłączeniu w on_key_release)
+ # - teleport na środek ekranu
+ # Ważne: stan trzymamy w self., żeby inne metody też go widziały.
+
+ def on_key_release(self, symbol: int, modifiers: int):
+ """
+ Puszczenie klawisza ZERUJE prędkość w danej osi.
+ Bez tej metody gracz po jednym naciśnięciu jechałby w nieskończoność.
+ """
+ if symbol in (arcade.key.LEFT, arcade.key.RIGHT):
+ self.player.change_x = 0
+ elif symbol in (arcade.key.UP, arcade.key.DOWN):
+ self.player.change_y = 0
+
+
+def main():
+ MyGame()
+ arcade.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dzien2/01_monety_start.py b/dzien2/01_monety_start.py
new file mode 100644
index 0000000..9f004d0
--- /dev/null
+++ b/dzien2/01_monety_start.py
@@ -0,0 +1,258 @@
+"""
+Dzień 2 — GŁÓWNE ZADANIE: własna hierarchia klas.
+
+Ten plik DZIAŁA od razu: jest gracz, 10 losowych monet, licznik punktów
+i wykrywanie kolizji. Uruchom go najpierw bez zmian:
+
+ python dzien2/01_monety_start.py
+
+Sterowanie: strzałki. Wyjście: Esc. Restart: R.
+
+===========================================================================
+ZADANIE
+===========================================================================
+
+1. Napisz klasę bazową `Enemy` dziedziczącą po `arcade.SpriteCircle`.
+ Wróg ma:
+ * konstruktor `__init__`, który przyjmuje promień, kolor i pozycję
+ startową, woła `super().__init__(...)` i ustawia prędkość
+ (`self.change_x`, `self.change_y`),
+ * nadpisaną metodę `update(self, delta_time=1/60, *args, **kwargs)`,
+ w której najpierw wołasz `super().update(delta_time)` (to przesuwa
+ sprite'a zgodnie z prędkością), a potem sprawdzasz krawędzie okna
+ i ODBIJASZ wroga: przy zderzeniu z lewą/prawą krawędzią zmieniasz
+ znak `change_x`, przy górnej/dolnej — znak `change_y`.
+
+2. Napisz DWIE klasy pochodne po `Enemy`, o RÓŻNYM zachowaniu. Każda musi
+ nadpisywać `update()` i wołać w nim `super().update(delta_time)`
+ (żeby odbijanie z klasy bazowej dalej działało), a potem dokładać coś
+ swojego. Przykładowe pomysły — wybierz dwa albo wymyśl własne:
+ * `Chaser` — powoli przyspiesza w stronę gracza (dostaje gracza
+ w konstruktorze i zapamiętuje go w atrybucie),
+ * `ZigZag` — co pewną liczbę klatek zmienia `change_y` na przeciwny,
+ * `Grower` — pulsuje: rośnie i maleje (odczyt: `self.scale_x`,
+ zapis: `self.scale = 1.2`; uwaga — `self.scale` przy
+ odczycie zwraca PARĘ wartości (x, y), nie jedną liczbę),
+ * `Sniper` — stoi w miejscu i co jakiś czas skacze w losowe miejsce.
+
+3. Dodaj do `setup()` listę `self.enemies = arcade.SpriteList()`, wstaw do
+ niej po kilka wrogów obu klas, rysuj ją w `on_draw()` i aktualizuj
+ w `on_update()` przez `self.enemies.update(delta_time)`.
+
+4. Obsłuż kolizję gracza z wrogiem (`arcade.check_for_collision_with_list`):
+ odejmij punkt / zakończ grę / cofnij gracza na środek — Twój wybór.
+ Napis "KONIEC GRY" i restart klawiszem R już są przygotowane
+ (patrz atrybut `self.koniec_gry`).
+
+DLACZEGO TAK: `self.enemies.update()` woła `update()` na każdym wrogu, ale
+KAŻDY wróg wykona SWOJĄ wersję tej metody — mimo że lista jest jedna i nie
+ma w niej żadnego `if`. To jest POLIMORFIZM: ten sam kod, różne zachowania,
+bo obiekty są różnych klas. Bez OOP musiałbyś trzymać typ wroga w polu
+`rodzaj` i pisać `if rodzaj == "chaser": ... elif rodzaj == "zigzag": ...`
+w każdym miejscu, które dotyka wrogów.
+"""
+
+import random
+
+import arcade
+
+SZEROKOSC = 800
+WYSOKOSC = 600
+TYTUL = "Dzień 2 — monety i wrogowie"
+
+PROMIEN_GRACZA = 20
+PREDKOSC_GRACZA = 5
+
+PROMIEN_MONETY = 10
+LICZBA_MONET = 10
+
+
+# ===========================================================================
+# PODPOWIEDZIANY ZARYS KLASY Enemy — odkomentuj i uzupełnij.
+#
+# Zwróć uwagę na sygnaturę update() w arcade 3.x: MUSI przyjmować
+# delta_time oraz *args/**kwargs, bo SpriteList woła update(delta_time)
+# na każdym elemencie. Stare tutoriale (arcade 2.x) pokazują `def update(self)`
+# bez argumentów — u nas to wysypie program.
+# ===========================================================================
+
+# class Enemy(arcade.SpriteCircle):
+# """Klasa BAZOWA wroga: porusza się i odbija od krawędzi okna."""
+#
+# def __init__(self, promien, kolor, center_x, center_y, change_x, change_y):
+# # super() woła konstruktor SpriteCircle — bez tego sprite nie ma
+# # ani tekstury, ani rozmiaru.
+# super().__init__(promien, kolor)
+# self.center_x = center_x
+# self.center_y = center_y
+# self.change_x = change_x
+# self.change_y = change_y
+#
+# def update(self, delta_time=1 / 60, *args, **kwargs):
+# # Najpierw zachowanie odziedziczone: przesunięcie o change_x/change_y.
+# super().update(delta_time, *args, **kwargs)
+#
+# # TODO: odbijanie od krawędzi.
+# # if self.left < 0 or self.right > SZEROKOSC:
+# # self.change_x = -self.change_x
+# # ... analogicznie dla bottom / top i change_y
+#
+#
+# class Chaser(Enemy):
+# """Klasa POCHODNA nr 1 — dopisuje własne zachowanie do bazowego."""
+#
+# def __init__(self, center_x, center_y, player):
+# super().__init__(15, arcade.color.RED, center_x, center_y, 2, 2)
+# self.player = player # zapamiętujemy CUDZY obiekt — to kompozycja
+#
+# def update(self, delta_time=1 / 60, *args, **kwargs):
+# super().update(delta_time, *args, **kwargs) # ruch + odbijanie
+# # TODO: delikatnie skręcaj w stronę gracza, np.
+# # if self.player.center_x > self.center_x:
+# # self.change_x += 0.05
+# # else:
+# # self.change_x -= 0.05
+#
+#
+# class ZigZag(Enemy):
+# """Klasa POCHODNA nr 2 — inne zachowanie, ta sama klasa bazowa."""
+#
+# def __init__(self, center_x, center_y):
+# super().__init__(12, arcade.color.PURPLE, center_x, center_y, 3, 0)
+# self.licznik = 0
+#
+# def update(self, delta_time=1 / 60, *args, **kwargs):
+# super().update(delta_time, *args, **kwargs)
+# # TODO: co 30 klatek zmień kierunek w pionie
+# # self.licznik += 1
+# # if self.licznik % 30 == 0:
+# # self.change_y = -self.change_y if self.change_y else 3
+
+
+class MyGame(arcade.Window):
+ """Okno gry."""
+
+ def __init__(self):
+ super().__init__(SZEROKOSC, WYSOKOSC, TYTUL)
+ self.background_color = arcade.color.DARK_BLUE
+
+ self.player_list = arcade.SpriteList()
+ self.coin_list = arcade.SpriteList()
+ # TODO (punkt 3 zadania): self.enemies = arcade.SpriteList()
+
+ self.player = None
+ self.punkty = 0
+ self.koniec_gry = False
+
+ self.setup()
+
+ def setup(self):
+ """Ustawia grę w stan początkowy. Wołane też przy restarcie (klawisz R)."""
+ self.punkty = 0
+ self.koniec_gry = False
+
+ # Gracz
+ self.player_list = arcade.SpriteList()
+ self.player = arcade.SpriteCircle(PROMIEN_GRACZA, arcade.color.AMAZON)
+ self.player.center_x = SZEROKOSC / 2
+ self.player.center_y = WYSOKOSC / 2
+ self.player_list.append(self.player)
+
+ # Monety w losowych miejscach
+ self.coin_list = arcade.SpriteList()
+ for _ in range(LICZBA_MONET):
+ moneta = arcade.SpriteCircle(PROMIEN_MONETY, arcade.color.GOLD)
+ moneta.center_x = random.randint(PROMIEN_MONETY, SZEROKOSC - PROMIEN_MONETY)
+ moneta.center_y = random.randint(PROMIEN_MONETY, WYSOKOSC - PROMIEN_MONETY)
+ self.coin_list.append(moneta)
+
+ # TODO (punkt 3 zadania): utwórz self.enemies i dodaj do niej wrogów,
+ # np. kilku Chaserów i kilka ZigZagów w losowych miejscach.
+ # Chaser potrzebuje referencji do gracza: Chaser(x, y, self.player)
+
+ def on_draw(self):
+ self.clear()
+
+ self.coin_list.draw()
+ self.player_list.draw()
+ # TODO (punkt 3 zadania): self.enemies.draw()
+
+ arcade.draw_text(
+ f"Punkty: {self.punkty}",
+ x=10,
+ y=WYSOKOSC - 30,
+ color=arcade.color.WHITE,
+ font_size=18,
+ )
+
+ if self.koniec_gry:
+ arcade.draw_text(
+ "KONIEC GRY — naciśnij R",
+ x=SZEROKOSC / 2,
+ y=WYSOKOSC / 2,
+ color=arcade.color.RED,
+ font_size=32,
+ anchor_x="center",
+ )
+
+ def on_update(self, delta_time: float):
+ if self.koniec_gry:
+ return
+
+ # arcade 3.x: lista sprite'ów odświeża się metodą update(delta_time).
+ # SpriteList nie ma metody on_update — to częsty błąd ze starych kursów.
+ self.player_list.update(delta_time)
+
+ # Trzymamy gracza w granicach okna
+ if self.player.left < 0:
+ self.player.left = 0
+ if self.player.right > SZEROKOSC:
+ self.player.right = SZEROKOSC
+ if self.player.bottom < 0:
+ self.player.bottom = 0
+ if self.player.top > WYSOKOSC:
+ self.player.top = WYSOKOSC
+
+ # Kolizje z monetami: funkcja zwraca LISTĘ trafionych sprite'ów.
+ zebrane = arcade.check_for_collision_with_list(self.player, self.coin_list)
+ for moneta in zebrane:
+ # remove_from_sprite_lists() usuwa sprite'a ze WSZYSTKICH list,
+ # do których należy — nie trzeba pamiętać, gdzie go włożyliśmy.
+ moneta.remove_from_sprite_lists()
+ self.punkty += 1
+
+ # TODO (punkty 3 i 4 zadania):
+ # 1) self.enemies.update(delta_time)
+ # — każdy wróg wykona SWOJĄ wersję update(), bo są różnych klas
+ # 2) trafieni = arcade.check_for_collision_with_list(self.player, self.enemies)
+ # if trafieni:
+ # self.koniec_gry = True # albo: odejmij punkt, cofnij gracza...
+
+ def on_key_press(self, symbol: int, modifiers: int):
+ if symbol == arcade.key.LEFT:
+ self.player.change_x = -PREDKOSC_GRACZA
+ elif symbol == arcade.key.RIGHT:
+ self.player.change_x = PREDKOSC_GRACZA
+ elif symbol == arcade.key.UP:
+ self.player.change_y = PREDKOSC_GRACZA
+ elif symbol == arcade.key.DOWN:
+ self.player.change_y = -PREDKOSC_GRACZA
+ elif symbol == arcade.key.R:
+ self.setup()
+ elif symbol == arcade.key.ESCAPE:
+ self.close()
+
+ def on_key_release(self, symbol: int, modifiers: int):
+ if symbol in (arcade.key.LEFT, arcade.key.RIGHT):
+ self.player.change_x = 0
+ elif symbol in (arcade.key.UP, arcade.key.DOWN):
+ self.player.change_y = 0
+
+
+def main():
+ MyGame()
+ arcade.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dzien3_4_platformowka/README.md b/dzien3_4_platformowka/README.md
new file mode 100644
index 0000000..c7ea21f
--- /dev/null
+++ b/dzien3_4_platformowka/README.md
@@ -0,0 +1,140 @@
+# Dni 3–4 — platformówka wg oficjalnego tutoriala
+
+Podstawa: **Simple Platformer** —
+
+
+Tutorial jest po angielsku i ma 14 kroków. Robimy go po kolei, ale **nie kopiuj
+całych plików** — przepisuj kod ręcznie do `gra.py` i po każdym kroku uruchamiaj:
+
+```bash
+python dzien3_4_platformowka/gra.py
+git add -A && git commit -m "dzien3: krok 5 — grawitacja"
+```
+
+Assety (grafika, dźwięki) — tutorial używa paczki Kenney'a, pobierz z
+ do katalogu `assets/`. Arcade ma też wbudowane
+zasoby dostępne przez ścieżki `":resources:images/..."` — na start to wystarczy
+i nic nie trzeba pobierać.
+
+---
+
+## Dzień 3 — gracz, platformy, fizyka, skok, kamera
+
+Kroki 1–7 tutoriala.
+
+- [ ] **Krok 1 — Install and Open a Window.** Klasa okna, `__init__` z `super()`,
+ `on_draw()` z `self.clear()`. To już umiesz z dnia 1 — przelatujemy szybko.
+- [ ] **Krok 2 — Textures and Sprites.** `arcade.Sprite(ścieżka, scale)`.
+ Zwróć uwagę: teraz sprite ma obrazek, a nie kolorowe koło.
+- [ ] **Krok 3 — Many Sprites with SpriteList.** Osobne listy: `player_list`,
+ `wall_list`. Ułóż podłogę w pętli `for x in range(0, 1250, 64)`.
+- [ ] **Krok 4 — Add User Control.** `on_key_press` / `on_key_release`
+ ustawiają `change_x`.
+- [ ] **Krok 5 — Add Gravity.** `arcade.PhysicsEnginePlatformer(player_sprite,
+ walls=wall_list, gravity_constant=1.0)`.
+ **To jest KOMPOZYCJA:** okno *ma* silnik fizyki (`self.physics_engine`),
+ a nie *jest* silnikiem fizyki. Dziedziczenie = „jest czymś”,
+ kompozycja = „ma coś”. Nie myl tych dwóch.
+ Skok: w `on_key_press` sprawdź `self.physics_engine.can_jump()` i ustaw
+ `change_y = PLAYER_JUMP_SPEED`.
+ W `on_update()` wołaj `self.physics_engine.update()` — silnik sam
+ przesuwa gracza i zatrzymuje go na ścianach (nie wołaj tu
+ `player_list.update()`, bo ruch policzyłby się dwa razy).
+- [ ] **Krok 6 — Resetting.** Wydziel metodę `setup()` i zawołaj ją z `__init__`.
+ Dzięki temu restart gry to jedno wywołanie.
+- [ ] **Krok 7 — Adding a Camera.** W arcade 3.x klasa nazywa się
+ `arcade.Camera2D` (stare tutoriale pokazują `arcade.Camera` — to 2.x).
+ Dwie kamery: jedna śledzi gracza (świat), druga stoi w miejscu (GUI/punkty).
+
+**Koniec dnia 3:** gracz biega po platformach, skacze, kamera jedzie za nim.
+Commit: `dzien3: platformowka - ruch, skok, kamera`.
+
+---
+
+## Dzień 4 — monety, dźwięki, wrogowie, poziomy, ekran końca
+
+Kroki 8–14 tutoriala.
+
+- [ ] **Krok 8 — Collecting Coins.** `arcade.check_for_collision_with_list`
+ + `remove_from_sprite_lists()` (znasz z dnia 2).
+- [ ] **Krok 9 — Adding Sound.** `arcade.load_sound(...)` w `setup()`,
+ `arcade.play_sound(...)` przy zebraniu monety i przy skoku.
+ Dźwięki wczytuj RAZ w `setup()`, nie w `on_update()` — inaczej gra zacznie
+ zwalniać.
+- [ ] **Krok 10 — Adding a Score.** Licznik punktów + `arcade.draw_text` na
+ kamerze GUI.
+- [ ] **Krok 11 — Using a Scene.** `arcade.Scene` zamiast ręcznie trzymanych
+ list. To znowu kompozycja: scena *ma* listy sprite'ów i wie, w jakiej
+ kolejności je rysować.
+- [ ] **Krok 12 — Loading a Map From a Map Editor.** Mapa z pliku `.tmx`
+ (edytor [Tiled](https://www.mapeditor.org/)), `arcade.load_tilemap`.
+- [ ] **Krok 13 — More Types of Layers.** Warstwy: platformy ruchome, kolce,
+ tło.
+- [ ] **Krok 14 — Multiple Levels.** Numer poziomu w atrybucie `self.level`,
+ wczytywanie pliku `mapa_{self.level}.tmx`.
+
+### Zadania własne (poza tutorialem)
+
+- [ ] **Wrogowie.** Wykorzystaj klasę `Enemy` z dnia 2 — przenieś ją tutaj
+ i dostosuj do platformówki (np. wróg chodzi po platformie i zawraca na
+ jej końcu). Kolizja gracza z wrogiem = utrata życia.
+- [ ] **Ekran końca gry.** Zrób go przez `arcade.View`: klasa `GameView`
+ (rozgrywka) i klasa `GameOverView` (napis + „naciśnij ENTER”).
+ Przełączanie: `self.window.show_view(GameOverView())`.
+ To przedsmak dnia 5 — stany gry jako osobne klasy.
+
+- [ ] **ZADANIE OOP DNIA — `property health` (0–100).**
+
+ Dodaj graczowi punkty życia tak, żeby **nie dało się** ustawić wartości
+ spoza zakresu 0–100 — nawet przez pomyłkę w innym miejscu kodu.
+
+ ```python
+ class Player(arcade.Sprite):
+ def __init__(self, sciezka, scale):
+ super().__init__(sciezka, scale)
+ self._health = 100 # podkreślnik = "to jest wewnętrzne, nie ruszaj z zewnątrz"
+
+ @property
+ def health(self):
+ """Getter — działa jak zwykły odczyt atrybutu: print(gracz.health)"""
+ return self._health
+
+ @health.setter
+ def health(self, wartosc):
+ """Setter — uruchamia się przy `gracz.health = 120` i pilnuje zakresu."""
+ # TODO: przytnij wartość do przedziału 0-100
+ # (podpowiedź: max(0, min(100, wartosc)))
+ # TODO: jeśli po przycięciu jest 0 — ustaw self.zywy = False
+ ...
+ ```
+
+ Sens `property`: z zewnątrz `gracz.health -= 30` wygląda jak zwykłe działanie
+ na atrybucie, ale pod spodem wykonuje się TWÓJ kod z walidacją. Nie trzeba
+ pisać `gracz.set_health(gracz.get_health() - 30)` ani pamiętać o sprawdzaniu
+ zakresu w każdym miejscu, gdzie gracz obrywa.
+
+ Sprawdź, że działa:
+ ```python
+ gracz.health = 150 # -> 100
+ gracz.health = -20 # -> 0, i gracz przestaje żyć
+ ```
+
+ - [ ] Narysuj pasek życia (`arcade.draw_lbwh_rectangle_filled`) o szerokości
+ proporcjonalnej do `gracz.health`.
+
+**Koniec dnia 4:** kompletna platformówka z monetami, dźwiękiem, wrogami,
+paskiem życia i ekranem końca.
+Commit: `dzien4: platformowka - monety, dzwieki, wrogowie, health property`.
+
+---
+
+## Ściąga — API arcade 3.x
+
+| Co | Jak |
+| ---------------------- | --------------------------------------------------------------- |
+| czyszczenie ekranu | `self.clear()` (nie `arcade.start_render()`) |
+| odświeżenie listy | `sprite_list.update(delta_time)` (nie `on_update`) |
+| metoda w klasie sprite | `def update(self, delta_time=1/60, *args, **kwargs):` |
+| kamera | `arcade.Camera2D` (nie `arcade.Camera`) |
+| prostokąt | `arcade.draw_lrbt_rectangle_filled` / `draw_lbwh_rectangle_filled` |
+| kolor tła | `self.background_color = arcade.color.X` |
diff --git a/dzien3_4_platformowka/gra.py b/dzien3_4_platformowka/gra.py
new file mode 100644
index 0000000..ec550d4
--- /dev/null
+++ b/dzien3_4_platformowka/gra.py
@@ -0,0 +1,167 @@
+"""
+Dni 3-4 — platformówka. Szkielet do rozbudowy.
+
+To jest CELOWO prawie pusty plik: przez dwa dni wypełniasz go krok po kroku
+według oficjalnego tutoriala (lista kroków w README.md obok).
+
+Uruchom teraz — otworzy się puste okno z podpowiedzią. To znaczy, że
+środowisko działa i można zaczynać.
+
+ python dzien3_4_platformowka/gra.py
+"""
+
+import arcade
+
+SZEROKOSC = 1280
+WYSOKOSC = 720
+TYTUL = "Platformówka"
+
+# Skala i stałe fizyki — przydadzą się od kroku 4 tutoriala.
+SKALA_KAFELKA = 0.5
+SKALA_GRACZA = 1.0
+PREDKOSC_GRACZA = 5
+GRAWITACJA = 1
+SILA_SKOKU = 20
+
+
+class GameView(arcade.View):
+ """
+ Widok rozgrywki.
+
+ Dziedziczymy po arcade.View, a nie po arcade.Window, bo w dniu 4 dojdzie
+ drugi widok (ekran końca gry) i będziemy się między nimi przełączać przez
+ self.window.show_view(...). Jedno okno, wiele widoków.
+
+ arcade.View ma te same metody-haki co Window: on_draw, on_update,
+ on_key_press, on_key_release — plus on_show_view (wołane, gdy widok
+ staje się aktywny).
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.background_color = arcade.color.DARK_BLUE
+
+ # --- Miejsce na atrybuty gry ---------------------------------------
+ # Deklarujemy je (na razie jako None / puste listy) już teraz, żeby
+ # z jednego miejsca było widać, z czego składa się gra.
+
+ # Krok 3: listy sprite'ów (albo od kroku 11: self.scene = None)
+ self.player_list = None
+ self.wall_list = None
+
+ # Krok 2-3: gracz
+ self.player_sprite = None
+
+ # Krok 5: silnik fizyki — KOMPOZYCJA: widok MA silnik, nie JEST silnikiem
+ self.physics_engine = None
+
+ # Krok 7: kamery — jedna jedzie za graczem, druga stoi (napisy)
+ self.camera = None
+ self.camera_gui = None
+
+ # Krok 10: punkty
+ self.punkty = 0
+
+ self.setup()
+
+ def setup(self):
+ """
+ Ustawia grę w stan początkowy — wołane z __init__ oraz przy restarcie
+ (krok 6 tutoriala). Cała zawartość poziomu powstaje TUTAJ, nie w __init__.
+ """
+ self.punkty = 0
+
+ # TODO krok 3: self.player_list = arcade.SpriteList()
+ # self.wall_list = arcade.SpriteList(use_spatial_hash=True)
+
+ # TODO krok 2: utwórz gracza, np.
+ # self.player_sprite = arcade.Sprite(
+ # ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png",
+ # scale=SKALA_GRACZA,
+ # )
+ # self.player_sprite.center_x = 64
+ # self.player_sprite.center_y = 128
+ # self.player_list.append(self.player_sprite)
+
+ # TODO krok 3: ułóż podłogę w pętli
+ # for x in range(0, 1250, 64):
+ # kafelek = arcade.Sprite(":resources:images/tiles/grassMid.png",
+ # scale=SKALA_KAFELKA)
+ # kafelek.center_x = x
+ # kafelek.center_y = 32
+ # self.wall_list.append(kafelek)
+
+ # TODO krok 5: self.physics_engine = arcade.PhysicsEnginePlatformer(
+ # self.player_sprite,
+ # walls=self.wall_list,
+ # gravity_constant=GRAWITACJA,
+ # )
+
+ # TODO krok 7: self.camera = arcade.Camera2D()
+ # self.camera_gui = arcade.Camera2D()
+
+ def on_draw(self):
+ """Rysowanie. Wołane automatycznie ~60 razy na sekundę."""
+ self.clear() # arcade 3.x — NIE arcade.start_render()
+
+ # TODO krok 7: self.camera.use() <- od tego miejsca rysujemy "w świecie"
+ # TODO krok 3: self.wall_list.draw()
+ # self.player_list.draw()
+
+ # TODO krok 7: self.camera_gui.use() <- napisy: nie jadą razem z kamerą
+ # TODO krok 10: arcade.draw_text(f"Punkty: {self.punkty}", 10, 10,
+ # arcade.color.WHITE, 18)
+
+ arcade.draw_text(
+ "Pusty szkielet — otwórz README.md obok i zacznij od kroku 1",
+ x=SZEROKOSC / 2,
+ y=WYSOKOSC / 2,
+ color=arcade.color.WHITE,
+ font_size=20,
+ anchor_x="center",
+ )
+
+ def on_update(self, delta_time: float):
+ """Logika gry. Tu zmieniamy stan, w on_draw tylko go pokazujemy."""
+ # TODO krok 5: self.physics_engine.update()
+ # Uwaga: gdy używasz silnika fizyki, NIE wołaj player_list.update() —
+ # silnik sam przesuwa gracza. Inaczej ruch policzy się dwa razy.
+
+ # TODO krok 8: kolizje z monetami (check_for_collision_with_list)
+ # TODO krok 7: przesuń kamerę za graczem
+ pass
+
+ def on_key_press(self, symbol: int, modifiers: int):
+ # TODO krok 4: strzałki LEFT/RIGHT -> self.player_sprite.change_x
+ # TODO krok 5: skok
+ # if symbol == arcade.key.UP and self.physics_engine.can_jump():
+ # self.player_sprite.change_y = SILA_SKOKU
+ if symbol == arcade.key.ESCAPE:
+ self.window.close()
+
+ def on_key_release(self, symbol: int, modifiers: int):
+ # TODO krok 4: zeruj change_x po puszczeniu strzałki
+ pass
+
+
+# TODO dzień 4: druga klasa widoku — ekran końca gry.
+#
+# class GameOverView(arcade.View):
+# def on_draw(self):
+# self.clear()
+# arcade.draw_text("KONIEC GRY - ENTER", SZEROKOSC / 2, WYSOKOSC / 2,
+# arcade.color.WHITE, 40, anchor_x="center")
+#
+# def on_key_press(self, symbol, modifiers):
+# if symbol == arcade.key.ENTER:
+# self.window.show_view(GameView()) # nowa gra od zera
+
+
+def main():
+ okno = arcade.Window(SZEROKOSC, WYSOKOSC, TYTUL)
+ okno.show_view(GameView())
+ arcade.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dzien5_projekt/WYMAGANIA.md b/dzien5_projekt/WYMAGANIA.md
new file mode 100644
index 0000000..23067e1
--- /dev/null
+++ b/dzien5_projekt/WYMAGANIA.md
@@ -0,0 +1,135 @@
+# Dzień 5 — własna gra od zera
+
+Projekt zaliczeniowy. Piszesz **własną** grę — nie przepisujesz tutoriala.
+Pomysł jest Twój; poniżej są tylko wymagania techniczne, które kod musi spełnić.
+
+## Zakres
+
+Gra ma być mała i skończona. Lepiej prosta gra, która działa i ma ekran końca,
+niż ambitna, która się nie uruchamia. Przykłady o dobrym rozmiarze:
+
+- uciekaj przed ścigającymi Cię wrogami i zbieraj punkty,
+- prosty shooter (statek strzela, przeciwnicy lecą z góry),
+- wąż / snake z przeszkodami,
+- gra zręcznościowa: unikaj spadających obiektów,
+- mini-platformówka z jednym poziomem i bossem.
+
+## Wymagania obowiązkowe
+
+### 1. Minimum 3 własne klasy
+
+Nie licząc klas widoków. Np. `Player`, `Enemy`, `Bullet`, `PowerUp`.
+
+### 2. Jedna hierarchia dziedziczenia: baza + co najmniej 2 klasy pochodne
+
+```
+ Enemy <- klasa bazowa: wspólne dane i wspólne zachowanie
+ / \
+ Chaser Shooter <- klasy pochodne: każda nadpisuje update() inaczej
+```
+
+Warunki:
+- klasa bazowa musi mieć **realną treść** — wspólny `__init__` i wspólną
+ implementację `update()`, a nie samo `pass`,
+- każda klasa pochodna nadpisuje przynajmniej jedną metodę i woła w niej
+ `super().(...)`,
+- wszystkie obiekty tej hierarchii trzymasz w **jednej** `SpriteList`
+ i aktualizujesz jednym `lista.update(delta_time)` — bez `if`-a po typie.
+ To jest sens polimorfizmu: dodanie trzeciego rodzaju wroga nie wymaga
+ zmiany ani jednej linii w pętli gry.
+
+Pamiętaj o sygnaturze z arcade 3.x:
+```python
+def update(self, delta_time=1 / 60, *args, **kwargs):
+ super().update(delta_time, *args, **kwargs)
+ ...
+```
+
+### 3. Minimum jedno `property` z walidacją
+
+Np. `health` (0–100), `ammo` (nie mniej niż 0), `poziom_trudnosci` (1–5),
+`speed` (z górnym limitem).
+
+```python
+@property
+def ammo(self):
+ return self._ammo
+
+@ammo.setter
+def ammo(self, wartosc):
+ if wartosc < 0:
+ raise ValueError("Amunicja nie może być ujemna")
+ self._ammo = min(wartosc, self.max_ammo)
+```
+
+Walidacja musi **coś robić** — przycinać zakres, rzucać wyjątek albo wywoływać
+efekt uboczny (np. `health` spadające do 0 przełącza grę na ekran końca).
+Samo `return self._x` bez settera nie liczy się jako walidacja.
+
+### 4. Stany gry przez `arcade.View`
+
+Minimum trzy widoki, każdy jako osobna klasa:
+
+- `MenuView` — ekran startowy („naciśnij ENTER, żeby zacząć”),
+- `GameView` — właściwa rozgrywka,
+- `GameOverView` — wynik + możliwość powtórki.
+
+Przełączanie: `self.window.show_view(GameView())`.
+Nie rób stanów przez `if self.stan == "menu"` w jednym wielkim `on_draw()` —
+o to właśnie chodzi w tym wymaganiu.
+
+### 5. Kod w wielu plikach w `src/`
+
+Struktura, do której dążymy:
+
+```
+dzien5_projekt/
+├── WYMAGANIA.md
+├── main.py # tylko uruchomienie: tworzy okno i pokazuje MenuView
+└── src/
+ ├── __init__.py
+ ├── settings.py # stałe: rozmiar okna, prędkości, kolory
+ ├── entities.py # Player, Enemy + klasy pochodne, Bullet...
+ └── views.py # MenuView, GameView, GameOverView
+```
+
+Import między plikami:
+```python
+from src.settings import SZEROKOSC, WYSOKOSC
+from src.entities import Player, Chaser
+```
+
+Uruchamiasz z katalogu `dzien5_projekt/`:
+```bash
+python main.py
+```
+
+Podział na pliki jest sensowny wtedy, gdy `entities.py` **nie musi** nic
+wiedzieć o `views.py`. Jeśli oba pliki importują się nawzajem — to znak, że
+podział jest zły; przenieś wspólne rzeczy do `settings.py`.
+
+## Sposób pracy
+
+1. Najpierw narysuj na kartce, jakie masz klasy i co która trzyma.
+2. Zrób `settings.py` i `main.py`, które otwierają puste okno. **Commit.**
+3. Dodaj `MenuView` i `GameView` z pustą planszą. **Commit.**
+4. Dodaj gracza, ruch. **Commit.**
+5. Dodaj hierarchię wrogów. **Commit.**
+6. Dodaj `property`, kolizje, punkty, `GameOverView`. **Commit.**
+7. Dopiero na końcu — dźwięki, grafika z , efekty.
+
+## Checklista przed oddaniem
+
+- [ ] `python main.py` uruchamia się bez błędów
+ (`PerformanceWarning: draw_text is an extremely slow function` to nie błąd —
+ arcade sugeruje obiekty `arcade.Text` zamiast `arcade.draw_text`;
+ przy naszej liczbie napisów można to zignorować)
+- [ ] 3+ własne klasy (bez widoków)
+- [ ] hierarchia: baza + 2 pochodne, każda z nadpisanym `update()` i `super()`
+- [ ] wrogowie w jednej `SpriteList`, aktualizowani bez `if`-a po typie
+- [ ] `property` z działającą walidacją (pokaż, że odrzuca złą wartość)
+- [ ] 3 widoki `arcade.View`, przełączanie działa w obie strony
+- [ ] kod w `src/`, `main.py` ma mniej niż 20 linii
+- [ ] historia gita: kilka commitów, każdy z działającym stanem gry
+- [ ] w kodzie nie ma zmiennych globalnych trzymających stan gry
+ (stałe z `settings.py` są OK — one się nie zmieniają)
diff --git a/dzien5_projekt/src/.gitkeep b/dzien5_projekt/src/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..162ba2f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+arcade>=3.0