scaffold: 5-dniowy kurs OOP + arcade

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oskar Kapala 2026-08-03 10:24:17 +02:00
parent 0acb692a18
commit 7c33b6bb63
11 changed files with 1052 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv/
__pycache__/
*.pyc
.idea/
.vscode/

View file

@ -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/` | 34 | 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 (515 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): <https://realpython.com/arcade-python-game-framework/>
- Kurs „Learn Arcade” (długi, po angielsku, z zadaniami): <https://learn.arcade.academy/>
- Tutorial platformówki (podstawa dni 34): <https://api.arcade.academy/en/latest/tutorials/platform_tutorial/index.html>
- Przykłady kodu (kopalnia gotowych rozwiązań): <https://api.arcade.academy/en/latest/example_code/index.html>
- Darmowe assety (grafika, dźwięki): <https://kenney.nl/assets>
## 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ć.

1
assets/README.md Normal file
View file

@ -0,0 +1 @@
Grafikę i dźwięki do gier pobierz z <https://kenney.nl/assets> (darmowe, licencja CC0) i wypakuj tutaj.

123
dzien1/01_okno.py Normal file
View file

@ -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 u siebie, podmieniamy tamtą wersję na własną.
Tej metody NIGDY nie wołamy sami arcade woła 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()

135
dzien1/02_sprite_ruch.py Normal file
View file

@ -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 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.<coś>, ż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()

258
dzien2/01_monety_start.py Normal file
View file

@ -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 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ż 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 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()

View file

@ -0,0 +1,140 @@
# Dni 34 — platformówka wg oficjalnego tutoriala
Podstawa: **Simple Platformer**
<https://api.arcade.academy/en/latest/tutorials/platform_tutorial/index.html>
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
<https://kenney.nl/assets> 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 17 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 814 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` (0100).**
Dodaj graczowi punkty życia tak, żeby **nie dało się** ustawić wartości
spoza zakresu 0100 — 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` |

View file

@ -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()

135
dzien5_projekt/WYMAGANIA.md Normal file
View file

@ -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().<metoda>(...)`,
- 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` (0100), `ammo` (nie mniej niż 0), `poziom_trudnosci` (15),
`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 <https://kenney.nl/assets>, 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ą)

View file

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
arcade>=3.0