From 7c33b6bb6313698dc237f524e91690e1d366e6af Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Mon, 3 Aug 2026 10:24:17 +0200 Subject: [PATCH] scaffold: 5-dniowy kurs OOP + arcade Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 5 + README.md | 87 +++++++++++ assets/README.md | 1 + dzien1/01_okno.py | 123 +++++++++++++++ dzien1/02_sprite_ruch.py | 135 +++++++++++++++++ dzien2/01_monety_start.py | 258 ++++++++++++++++++++++++++++++++ dzien3_4_platformowka/README.md | 140 +++++++++++++++++ dzien3_4_platformowka/gra.py | 167 +++++++++++++++++++++ dzien5_projekt/WYMAGANIA.md | 135 +++++++++++++++++ dzien5_projekt/src/.gitkeep | 0 requirements.txt | 1 + 11 files changed, 1052 insertions(+) create mode 100644 .gitignore create mode 100644 assets/README.md create mode 100644 dzien1/01_okno.py create mode 100644 dzien1/02_sprite_ruch.py create mode 100644 dzien2/01_monety_start.py create mode 100644 dzien3_4_platformowka/README.md create mode 100644 dzien3_4_platformowka/gra.py create mode 100644 dzien5_projekt/WYMAGANIA.md create mode 100644 dzien5_projekt/src/.gitkeep create mode 100644 requirements.txt 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