🥚 Jajko
0 / 43 lekcji
🐍

Zbuduj Snake
w Pythonie

44 lekcje — od pustego okna do pełnej gry z rekordem zapisywanym na dysk. Każda lekcja to działający program.

Prereq: Podstawy Pythona — znasz zmienne, listy, pętle i funkcje.
Nowe: Pygame, game loop, krotki, maszyna stanów, pliki.
📋

Sprawdź wiedzę — gotowość do Snake-Xtra

📋 Test gotowości — podstawy Pythona przed kursem Snake-Xtra
To nieobowiązkowa samoocena — nie egzamin. Nic nie blokuje: możesz ją pominąć i od razu kliknąć „Zacznij kurs”. Służy tylko Tobie — żebyś wiedział, czy warto coś sobie odświeżyć, zanim ruszysz.
21 pytań w 6 kategoriach, próg ≥70% per kategoria. Poniżej progu dostaniesz link do sekcji, którą warto powtórzyć. Nie martw się słabym wynikiem — kurs i tak tłumaczy wszystko od zera.
🅐 Zmienne i typy 🅑 Arytmetyka 🅒 Warunki 🅓 Listy 🅔 Pętle 🅕 Funkcje i losowanie
E00

Co zbudujesz — kamienie milowe

🎬 Zobacz grę w akcji — oto, co zbudujesz w tym kursie

Witaj w Snake XTRA. W tym kursie zbudujesz klasyczną grę Snake — ale nie pobierzesz gotowca ani nie przepiszesz cudzego szablonu. Napiszesz własny kod, linia po linii, rozumiejąc każdą z nich. Zaczniesz od pustego okna, a skończysz na kompletnej grze: wąż rośnie z każdym zjedzonym jabłkiem, przyspiesza na kolejnych poziomach, pojawia się ekran Game Over, a rekord zapisany na dysku przetrwa zamknięcie programu. A pętla gry, obsługa klawiszy i zapisywanie stanu, które tu poznasz, to fundament każdej interaktywnej aplikacji — nie tylko gier.

Zanim ruszysz, warto wiedzieć, jak zbudowany jest ten kurs — bo jego rytm jest celowy i będzie Ci towarzyszył przez wszystkie 44 kroki. To materiał na kilkanaście godzin spokojnej nauki — lekcja lub dwie dziennie to świetny rytm; liczy się regularność, nie tempo.

🎮 Oto Twój cel — tak wygląda gra, którą zbudujesz
WYNIK: 12     REKORD: 18
Zaczynasz od pustego okna (S01) i krok po kroku dochodzisz tutaj: wąż, który rośnie po jabłkach, punkty, rekord i rosnąca prędkość. To nie obrazek z internetu — to Twój kod na końcu kursu.

Kurs przeplata dwa rodzaje lekcji. Lekcje oznaczone literą T to teoria — każda bierze jedno pojęcie Pythona lub Pygame (krotkę, pętlę zdarzeń, kolizję) i rozkłada je na części: metafora, kod, eksperyment do samodzielnego sprawdzenia, zadania. Lekcje oznaczone literą S to Snake — w nich nie poznajesz nowej teorii, tylko składasz zdobyte narzędzia w działający fragment gry. Zasada jest prosta: T daje Ci narzędzie do ręki, S pokazuje, jak zbudować nim kawałek gry. Najpierw rozumiesz, potem budujesz.

Przez cały kurs pracujesz w jednym plikusnake.py. Zaczyna się on od kilkunastu linii (samo okno), a kończy na ponad setce (pełna gra). Nie zaczynasz od zera przy każdej lekcji: każda lekcja S dokłada nowe linie do tego, co już masz. Twój plik rośnie razem z grą — i pod koniec trzymasz w ręku grę, której każdy fragment napisałeś samodzielnie. W lekcjach S będziemy często prosić, żebyś pisał z pamięci, a dopiero potem porównywał kod ze wzorcem — bo między „rozumiem, gdy czytam” a „umiem napisać sam” jest przepaść, którą zasypuje tylko praktyka.

🏁 Kamienie milowe — co zobaczysz po każdej lekcji S:
  • 🪟 Po S01 → działające zielone okno (program działa, możesz zamknąć)
  • Po S02 → ruchomy kwadrat — głowa węża porusza się po siatce
  • ⌨️ Po S03 → sterowanie strzałkami, bez możliwości zawrócenia
  • 🐍 Po S04/S05 → ciało węża podąża za głową (lista krotek!)
  • 🍎 Po S06 → czerwone jabłko losowane na planszy
  • 📈 Po S07 → wąż rośnie po zjedzeniu, punkty w tytule okna
  • 💥 Po S08 → jest śmierć — ściany i własne ciało kończą grę
  • 🔄 Po S09 → ekran Game Over z wynikiem, restart klawiszem R
  • 🏆 Po S10 → finalna gra: siatka, HUD, rosnąca prędkość
  • 🎁 BS1/BS2 → bonus: refaktoring kodu + rekord zapisywany na dysku
Czego potrzebujesz na start: ukończonego kursu Podstawy Pythona — albo równoważnej wiedzy. Powinieneś czuć się swobodnie z listami, pętlami (for i while) oraz funkcjami, bo Snake opiera się na nich od pierwszych lekcji. Jeśli któreś z tych pojęć jest mgliste — wróć najpierw do podstaw, a tutaj poradzisz sobie znacznie łatwiej.
Nie masz pewności, czy Twoje podstawy wystarczą? Możesz zrobić krótki, nieobowiązkowy test gotowości — otwórz test →. Albo po prostu ruszaj dalej — kurs tłumaczy wszystko od zera.
SETUP

Instalacja środowiska — Python + Pygame + VS Code

Zanim napiszesz pierwszą linię gry, potrzebujesz Pythona 3.12 (silnika, który uruchamia Twój kod), Pygame (biblioteki do grafiki i gier — więcej w T01) oraz edytora VS Code. Przygotujesz to raz — naszą instrukcją krok po kroku, ze zrzutem ekranu do każdego kliknięcia.

🛠️ Pełna instrukcja instalacji — krok po kroku
Python 3.12 + Pygame + VS Code (po polsku), każdy krok ze zrzutem ekranu. ~10 minut, bez administratora. Zrób ją raz i wróć tutaj.
👉 Otwórz instrukcję: devostro.pl/start →
Masz już środowisko? To sprawdź w terminalu (w VS Code skrótem Ctrl+`) te dwie rzeczy, zanim ruszysz dalej:
python --version
python -m pip install pygame
> python --version
Python 3.12.10
> python -m pip install pygame
Successfully installed pygame-2.6.1
🍎🐧 Masz macOS albo Linuksa?
Kurs pokazuje komendy z Windows, ale Pygame działa wszędzie — różni się tylko wywołanie Pythona w terminalu:
  • macOS — zainstaluj Pythona z python.org (jak w kursie podstaw), a w aplikacji Terminal pisz python3 zamiast python: najpierw python3 -m pip install pygame, potem python3 snake.py.
  • Linux (Ubuntu/Mint) — raz: sudo apt install python3-pip, potem python3 -m pip install pygame i python3 snake.py.
Zasada na cały kurs: gdy w lekcji widzisz python, u Ciebie to python3. Cała reszta — kod, klawisze, pliki — jest identyczna.
Coś nie działa? 'python' is not recognized → przejdź instrukcję devostro.pl/start od początku i pamiętaj o zaznaczeniu „Add python.exe to PATH”. ModuleNotFoundError: No module named 'pygame' → VS Code odpala inny Python niż ten z Pygame: Ctrl+Shift+P → „Python: Select Interpreter” → wybierz tego, w którym instalowałeś Pygame.
Ostatni krok — plik gry. W folderze swojego projektu (np. snake) stwórz pusty plik snake.py — to w nim, krok po kroku, powstanie cała gra. Nie usuwaj go po lekcji; napiszemy go razem w T01.
✔️ Zanim przejdziesz dalej — sprawdź:
  • python --version pokazuje 3.12.x
  • python -m pip install pygame kończy się „Successfully installed pygame-2.6.1”
  • Masz pusty plik snake.py w folderze projektu
Środowisko gotowe: Python uruchamia kod, Pygame zainstalowany, a pusty snake.py czeka na pierwszą linię. Od tego momentu zaczynasz pisać własny kod: w T01 najpierw zobaczysz na żywo, czemu program bez jednego kluczowego elementu się wiesza, a potem sam napiszesz wersję, która działa — rozumiejąc każdą linię. Czas zacząć budować.
T01

Import i pierwsze okno

Wyobraź sobie profesjonalne studio nagraniowe — stół mikserski, instrumenty, mikrofony. Możesz przy tym wszystkim usiąść, ale zanim wydobędziesz jakikolwiek dźwięk, ktoś musi włączyć zasilanie i uruchomić sprzęt. import pygame to wejście do studia: mówisz Pythonowi, że potrzebujesz zestawu narzędzi do budowania gier. pygame.init() to włączenie zasilania — uruchomienie grafiki, dźwięku i klawiatury. Dopiero po tych dwóch krokach możesz cokolwiek zrobić. Tak zaczyna się każdy program Pygame i każda gra, którą kiedykolwiek napiszesz.

Zanim wpiszesz pierwszą linię, warto zrozumieć, czym jest to, co za chwilę zaimportujesz. Pygame to biblioteka — gotowy zestaw funkcji napisany przez innych programistów. Jej twórcy spędzili lata, rozwiązując problemy, które nie mają nic wspólnego z mechaniką Twojej gry: jak otworzyć okno na Windows, Mac i Linux jednocześnie, jak zsynchronizować rysowanie z kartą graficzną, żeby ekran nie migotał, jak odczytywać klawiaturę bez opóźnień. Znasz ten mechanizm z kursu podstaw — import random dawał Ci od razu random.randint(), bez pisania własnego generatora liczb. Tutaj jedna linia importu daje Ci okna, rysowanie, klawiaturę i dźwięk. Ty zajmujesz się tym, co najciekawsze: budowaniem gry.

Zwróć jeszcze uwagę na kropkę w zapisach takich jak pygame.display.set_mode(). To ścieżka do funkcji, czytana od lewej: z pakietu pygame, z modułu display, wywołaj funkcję set_mode. Każdy taki zapis w tym kursie czytasz tak samo: skąd pochodzi, z którego modułu, co robi.


Każda gra zbudowana na Pygame — od najprostszej do najbardziej złożonej — korzysta z tych samych kroków w tej samej kolejności. To nie konwencja, tylko konsekwencja tego, jak działa grafika i system operacyjny. Gdy ten szkielet zapamiętasz, będziesz pisał własne gry bez zaglądania do kursu.

Najpierw pygame.init() budzi wszystkie podsystemy biblioteki — grafikę, dźwięk, obsługę wejścia. Potem pygame.display.set_mode() tworzy okno: podajesz wymiary w pikselach — w Snake kwadrat 600×600 — i dostajesz obiekt będący powierzchnią do rysowania. Ten obiekt, który nazwiesz ekran, będzie Ci towarzyszył przez cały kurs: każde ekran.fill() i każde pygame.draw.rect() trafi właśnie do niego. Przed pętlą główną definiujesz jeszcze stałe — nazwane wartości dla wymiarów, kolorów i prędkości; po co — zobaczysz za chwilę na własnym kodzie.

I wreszcie serce programu — pętla while True, która kręci się bez przerwy dopóki gra działa. W każdym obiegu trzy rzeczy dzieją się po kolei: sprawdź, co się stało od ostatniej klatki, oblicz nowy stan gry, narysuj go na ekranie. Tę trójfazową strukturę poznasz dokładnie w T02. Na razie zapamiętaj jedno: wszystko, co dojdzie do kodu w następnych lekcjach — ruch węża, jabłko, wynik — wejdzie dokładnie w jedno z tych trzech miejsc. Struktura się nie zmienia, zmienia się tylko zawartość.

⌨️ Krok 0 — Otwórz plik snake.py
Otwórz VS Code, wejdź do folderu projektu i kliknij snake.py — pusty plik z lekcji Setup. Cały kod tego kursu piszesz w tym jednym pliku. Nie tworzysz nowych plików, nie rozdzielasz na moduły. Jeden plik rośnie razem z grą, od początku do końca.

Najłatwiej zapamiętać, dlaczego coś jest potrzebne, gdy zobaczysz, co się dzieje bez tego. Dlatego zaczniesz od wersji celowo niepełnej — brakuje w niej jednego ważnego elementu. Najpierw zobaczysz efekt braku, potem napiszesz wersję kompletną, już rozumiejąc każdą linię. Wklej poniższy kod do snake.py, zapisz (Ctrl+S) i uruchom:

import pygame
pygame.init()

ekran = pygame.display.set_mode((600, 600))

while True:
    ekran.fill((15, 25, 15))
    pygame.display.flip()
> python snake.py
ciemnozielone okno 600×600 — ale nie można go zamknąć!

Okno się pojawiło — ale teraz kliknij X. Nic się nie dzieje. Po chwili okno może dostać oznaczenie „(Not Responding)”. Jedyne wyjście to Ctrl+C w terminalu.

Co tu się dzieje? Program kręci się w pętli: zamaluj ekran, wyświetl, zamaluj, wyświetl — setki razy na sekundę. W każdym obiegu powinien zapytać system: czy coś się zdarzyło? Ktoś kliknął X? W tym kodzie tego pytania nie ma. System wysyła komunikat o kliknięciu, program go nie odbiera, komunikaty piętrzą się w kolejce — i po chwili system uznaje program za zawieszony. Zapamiętaj to doświadczenie: ten jeden brakujący element to najczęstsza przyczyna zamrożonych okien u początkujących.

🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): Jaki tytuł ma okno, które właśnie otworzyłeś?
Odpowiedź
Domyślny: pygame window. Za chwilę ustawisz własny przez pygame.display.set_caption("SNAKE") — napis pojawi się na pasku tytułowym okna.

Czas naprawić program — tym razem rozumiejąc każdą nową linię. Do kodu wejdą trzy elementy.

Pierwszy to nazwane stałe zamiast gołych liczb. W poprzedniej wersji masz set_mode((600, 600)) — dwie liczby bez nazwy. Za miesiąc nie będziesz pamiętał, co oznacza pierwsze 600, a co drugie. Programiści nazywają takie liczby magic numbers: mają sens w chwili pisania, potem stają się zagadką. Rozwiązanie — dajesz wartości nazwę: SZEROKOSC = 600. Od tej chwili wszędzie używasz nazwy, a liczba siedzi w jednym miejscu na górze pliku. Zmiana rozmiaru okna to edycja jednego wiersza, nie szukanie po całym pliku.

Drugi to obsługa zamknięcia okna. pygame.event.get() zwraca listę zdarzeń, które zaszły od ostatniej klatki — kliknięcia, klawisze, zamknięcie okna. Przeglądasz ją pętlą for i reagujesz na to, co Cię interesuje. Kliknięcie X generuje zdarzenie pygame.QUIT — wtedy wykonujesz dwa kroki: pygame.quit() zwalnia zasoby Pygame, a raise SystemExit kończy program Pythona. Dwie linie, bo to dwie osobne czynności: sprzątanie po bibliotece i wyjście z programu.

Trzeci to tytuł okna — jedna linia: pygame.display.set_caption("SNAKE"). Zastąp cały plik tym kodem i uruchom:

import pygame
pygame.init()

SZEROKOSC = 600
WYSOKOSC  = 600
KOLOR_TLO = (15, 25, 15)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")

while True:
    for event in pygame.event.get():          # opróżniaj kolejkę zdarzeń każdą klatkę
        if event.type == pygame.QUIT:
            pygame.quit()                     # zwolnij zasoby Pygame
            raise SystemExit                  # zakończ program

    ekran.fill(KOLOR_TLO)                     # wyczyść ekran (nowa klatka)
    pygame.display.flip()                     # wyślij gotową klatkę na monitor
> python snake.py
okno z tytułem "SNAKE" — można zamknąć klikając X ✓

Działa. Tytuł na pasku, kliknięcie X kończy program porządnie. Zwróć uwagę na strukturę: wszystko przed while True to inicjalizacja — wykonuje się raz przy starcie. Wewnątrz pętli każda klatka robi to samo: odbiera zdarzenia, czyści ekran, wyświetla. Ta architektura zostaje do końca kursu — dodasz dziesiątki linii, ale każda trafi w jedno z tych miejsc. Szkielet jest gotowy, teraz będziesz go wypełniał.

Jednej rzeczy w tym kodzie jeszcze nie ma: kontroli czasu. Pętla kręci się tak szybko, jak pozwala procesor — tysiąc klatek na sekundę na szybkim komputerze, sto na wolnym. W T03 dodasz zegar.tick(8), który ustali równe 8 klatek na sekundę — bo wąż ma się poruszać w kontrolowanym tempie, nie „ile zdąży”. Na razie to nie przeszkadza: okno jest puste i nie ma co animować.

📄 Dlaczego SZEROKOSC wielką literą? (klik)

To umowa programistów, nie reguła języka. Nazwy WIELKIMI_LITERAMI oznaczają stałe — wartości, które definiujesz raz i nie zmieniasz podczas działania programu. Python nie zabroni Ci napisać SZEROKOSC = 800 w środku kodu, ale każdy czytający — łącznie z Tobą za kilka tygodni — od razu zobaczy, że coś jest nie tak. Wielkie litery to sygnał: „tej wartości nie ruszaj, ona jest fundamentem”. W Snake będziesz miał stałe dla rozmiaru okna, rozmiaru kratki, kolorów i prędkości — wszystkie WIELKIMI, wszystkie na górze pliku, zanim cokolwiek innego.

Zostało jedno pojęcie: RGB, czyli język kolorów w Pygame. Każdy piksel monitora świeci trzema diodami — czerwoną (Red), zieloną (Green) i niebieską (Blue) — a każda z nich ma jasność od 0 (wyłączona) do 255 (maksimum). Trzy liczby opisują dowolny kolor: (0, 0, 0) to czarny — brak światła. (255, 255, 255) to biały — wszystko na maksimum. (255, 0, 0) to czysty czerwony — świeci tylko jedna dioda. A tło planszy Snake, (15, 25, 15), to bardzo ciemna zieleń — odrobina każdego kanału z przewagą zielonego.

Trzy liczby w nawiasach okrągłych to w Pythonie krotka — niezmienny zestaw wartości. O krotkach dokładnie w T10, ale już teraz widzisz je przy każdym kolorze i rozmiarze okna. Zwróć uwagę na podwójny nawias w set_mode((600, 600)): zewnętrzny to wywołanie funkcji, wewnętrzny to krotka będąca argumentem. Ten wzorzec jeszcze wróci.

Kolory, których użyjesz w Snake — warto je znać od początku:

  • (15, 25, 15) — ciemnozielony — tło planszy
  • (80, 200, 80) — zielony — ciało węża
  • (220, 60, 60) — czerwony — jabłko
  • (40, 120, 40) — ciemnozielony — linie siatki
  • (255, 255, 255) — biały — tekst i UI
🧪 Eksperyment — mieszaj kolory

Spędź 3–4 minuty na eksperymentowaniu. Zmień wartość KOLOR_TLO, zapisz (Ctrl+S) i uruchom. Światło miesza się inaczej niż farby — a to doświadczenie zostanie w głowie dłużej niż czytanie o nim.

  1. Spróbuj (200, 0, 0) — tylko czerwony kanał aktywny. Teraz (0, 0, 200) — tylko niebieski. Jaka różnica w wyglądzie?
  2. Spróbuj (200, 200, 0) — czerwony i zielony razem, bez niebieskiego. Jakiego koloru się spodziewałeś? Jakiego dostałeś?
  3. Spróbuj (80, 80, 80) i (180, 180, 180) — równe wartości we wszystkich kanałach. Co dostaniesz?
  4. Znajdź własny kolor, który Ci się podoba i zapamiętaj go.
Odpowiedzi

(200, 0, 0) = ciemnoczerwone, (0, 0, 200) = ciemnoniebieskie.

(200, 200, 0) = żółty — czerwony + zielony = żółty. To nieintuicyjne dla kogoś kto mieszał farby, ale światło miesza się inaczej niż pigmenty.

Równe wartości we wszystkich kanałach = odcień szarości. Małe liczby = ciemna szarość, duże = jasna. (128, 128, 128) = neutralna szarość środkowa.

✏️ Zadania samodzielne
  1. Zmień tytuł okna na swoje imię. Zapisz i uruchom — sprawdź, że pasek tytułu się zmienił.
  2. Usuń cały blok for event in pygame.event.get(): wraz z obsługą QUIT. Uruchom i spróbuj zamknąć okno. Obserwuj co się dzieje i kiedy. Przywróć kod do działającego stanu.
  3. Zmień KOLOR_TLO na kolor, który znalazłeś w eksperymencie. Uruchom i sprawdź efekt.
  4. Własnymi słowami napisz komentarz w kodzie przy liniach pygame.quit() i raise SystemExit: co robi każda z nich i czym się różnią? To ćwiczenie zrozumienia, nie wpisywania.
✔️ Po T01 powinieneś umieć:
  • Napisać import pygame i pygame.init() oraz wyjaśnić, co każde robi i dlaczego kolejność ma znaczenie
  • Stworzyć okno z zadanym rozmiarem i tytułem
  • Obsłużyć zdarzenie QUIT — przez pygame.quit() i raise SystemExit — i wyjaśnić, dlaczego to dwa oddzielne kroki
  • Definiować stałe WIELKIMI_LITERAMI i wyjaśnić po co — czym są magic numbers i jak stałe je eliminują
  • Opisać dowolny kolor w formacie RGB i odczytać kolor z istniejącej krotki
Masz działające okno i rozumiesz każdą linię, która je tworzy. Ale pętla nie ma jeszcze wyraźnej struktury — zdarzenia i rysowanie stoją obok siebie bez wyznaczonych stref. W T02 nadasz jej trójfazową architekturę: ZDARZENIA, LOGIKA, RYSOWANIE — schemat, z którego korzysta każda gra na świecie. A w T03 dodasz kontrolę czasu, żeby wąż poruszał się dokładnie 8 razy na sekundę niezależnie od komputera.

Okno na razie jest puste — i tak ma być, budujesz silnik. Pierwszy kształt na planszy stanie już za 3 lekcje (T04), a ruszy za 5 (T06). Oto Twoja droga:
🗺️ Droga do ruchomego węża
Puste okno
▶ TERAZ
puste okno
Głowa węża
głowa
Ruchomy wąż
ruch →
🧠 Sprawdź wiedzę — T01
T02

Game loop — jak działa pętla gry

Wyobraź sobie animowany flip-book — notesik, w którym na każdej stronie rysujesz węża odrobinę przesuniętego, a potem szybko kartkujesz. Żadna strona się nie rusza, a jednak widzisz ruch, bo mózg skleja serię nieruchomych obrazków w płynną animację. Komputer robi dokładnie to samo: co klatkę czyści ekran, rysuje wszystko od nowa i wyświetla wynik. W grze nie ma żadnego „ruchu” — jest tylko bardzo szybkie kartkowanie. Pętla, która to kartkowanie napędza, nazywa się game loop i to o niej jest cała ta lekcja.

W T01 napisałeś już działający game loop — pętlę while True, która odbiera zdarzenia i odświeża ekran. Działa, ale wygląda jak jeden ciąg instrukcji bez wyraźnych granic. Teraz nadasz jej strukturę, bo każdy game loop na świecie — od Snake po najnowsze gry AAA — składa się z trzech faz wykonywanych co klatkę, zawsze w tej samej kolejności.

Najpierw ZDARZENIA: pytasz system, co się stało od ostatniej klatki — czy gracz nacisnął klawisz, kliknął myszą, zamknął okno. Potem LOGIKA: na podstawie tego, co się stało, obliczasz nowy stan gry — przesuwasz węża o kratkę, sprawdzasz, czy trafił w jabłko, aktualizujesz wynik. Na końcu RYSOWANIE: bierzesz świeżo obliczony stan i malujesz go na ekranie.

Ta kolejność nie jest umowna — wynika z logiki przyczyny i skutku. Gdybyś rysował przed logiką, pokazywałbyś graczowi stan z poprzedniej klatki: wąż zawsze „krok za” tym, gdzie naprawdę jest. Gdybyś zbierał zdarzenia po logice, naciśnięcie strzałki zadziałałoby dopiero w następnej klatce — przy 8 klatkach na sekundę to odczuwalne ósme części sekundy opóźnienia na każdy ruch. Zdarzenia, logika, rysowanie. Zawsze tak.

⌨️ Krok 0 — Otwórz kod z T01
Otwórz snake.py z kodem, który skończyłeś w T01 — stałe, okno z tytułem, pętla z obsługą QUIT. Niczego nie kasujesz: w tej lekcji nadasz temu kodowi strukturę i dodasz obsługę klawiatury.

Pierwszy krok jest prosty, ale ważniejszy niż wygląda: dodasz do pętli trzy komentarze, które wyznaczą strefy faz. Nie zmieniasz żadnej logiki — porządkujesz. Za kilka lekcji w strefie LOGIKA będzie dwadzieścia linii, w RYSOWANIE piętnaście, i bez tych granic wszystko zlałoby się w jedną masę, w której nie wiadomo, gdzie co wstawić. Zastąp swoją pętlę tą wersją:

import pygame
pygame.init()

SZEROKOSC = 600
WYSOKOSC  = 600
KOLOR_TLO = (15, 25, 15)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")

while True:
    # ─── 1. ZDARZENIA ─────────────────────────────────
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # ─── 2. LOGIKA ────────────────────────────────────
    # (na razie pusta — gra nic jeszcze nie oblicza)

    # ─── 3. RYSOWANIE ─────────────────────────────────
    ekran.fill(KOLOR_TLO)
    pygame.display.flip()
> python snake.py
identyczny efekt — ale kod jasno podzielony na 3 fazy

Okno wygląda tak samo jak przed chwilą — i o to chodzi. Zmieniła się architektura, nie zachowanie. Od teraz, gdy w kolejnych lekcjach przeczytasz „dodaj to do sekcji LOGIKA”, będziesz dokładnie wiedział, gdzie patrzeć. Pamiętasz z T01, co się działo, gdy w pętli zabrakło event.get() — okno zamarzało, bo kolejka zdarzeń systemu się przepełniała. Teraz widzisz to samo z lotu ptaka: faza ZDARZENIA jest jedyną, której nigdy nie wolno pominąć, nawet gdy LOGIKA jest pusta.

🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): Co się stanie, jeśli umieścisz blok RYSOWANIA przed LOGIKĄ?
Odpowiedź
Rysujesz stan z poprzedniej klatki zamiast aktualnego — wąż jest zawsze „krok za”. W pustym oknie różnicy nie widać, ale gdy w T06 dodasz ruch, opóźnienie o klatkę stanie się realne. Zawsze: ZDARZENIA → LOGIKA → RYSOWANIE.

Do tej pory Twoja gra reaguje tylko na jedno zdarzenie: zamknięcie okna. Czas nauczyć ją słuchać klawiatury — bo przecież wąż będzie sterowany strzałkami. Gdy gracz naciska dowolny klawisz, Pygame generuje zdarzenie typu pygame.KEYDOWN. Samo KEYDOWN mówi tylko „jakiś klawisz poszedł w dół” — który konkretnie, sprawdzasz w polu event.key. Pygame ma stałą dla każdego klawisza: K_ESCAPE to Escape, K_UP strzałka w górę, K_r litera R.

Zaczniesz od Escape jako drugiego sposobu zamykania gry — to dobra praktyka w każdej grze i najprostszy możliwy trening obsługi klawiatury, zanim w T07 te same mechanizmy posłużą do sterowania wężem. Rozbuduj sekcję ZDARZENIA do tej postaci:

    # ─── 1. ZDARZENIA ─────────────────────────────────
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:   # Escape zamyka grę
                pygame.quit()
                raise SystemExit
— naciśnij Escape —
program zamyka się tak samo jak kliknięciem X

Uruchom i naciśnij Escape — okno znika, dokładnie jak po kliknięciu X. Zwróć uwagę na zagnieżdżenie: najpierw sprawdzasz typ zdarzenia (KEYDOWN — coś naciśnięto), dopiero wewnątrz który klawisz (K_ESCAPE). Ten dwupoziomowy wzorzec wróci w T07 przy strzałkach: jeden warunek KEYDOWN, a w nim cztery porównania event.key — po jednym na kierunek.

Zamiast reakcji na klawisz — czerwony AttributeError: 'Event' object has no attribute 'key', często od samego ruszenia myszą? Sprawdzasz event.key bez wcześniejszego event.type == pygame.KEYDOWN — oba ify wylądowały na tym samym poziomie wcięcia. Przez kolejkę płyną też zdarzenia bez pola key (ruch myszy, QUIT) i pierwsze z nich wywraca program. Pilnuj zagnieżdżenia: najpierw typ zdarzenia, dopiero WEWNĄTRZ niego — który klawisz.
📋 Stałe klawiszy K_* — najważniejsze
  • K_ESCAPE — Escape
  • K_RIGHT, K_LEFT, K_UP, K_DOWN — strzałki
  • K_r, K_p, K_SPACE — litery i spacja

Nazwę dowolnego naciśniętego klawisza wypisze Ci pygame.key.name(event.key).

🔍 Czemu raise SystemExit a nie exit()?
exit() też działa, ale jest przeznaczony dla interaktywnych konsol. raise SystemExit to czystsze wyjście — gwarantuje, że interpreter Pythona poprawnie kończy. Zawsze poprzedź je pygame.quit(), które zwalnia pamięć karty graficznej.
🧪 Eksperyment — podejrzyj kolejkę zdarzeń

Twoja gra obsługuje dwa typy zdarzeń — ale ile ich naprawdę przepływa przez kolejkę? Zanim sprawdzisz, postaw hipotezę: jakie zdarzenia generuje samo używanie okna — klawisze, mysz, coś jeszcze? Dodaj tymczasowo print(event) do pętli zdarzeń:

for event in pygame.event.get():
    if event.type != pygame.MOUSEMOTION:   # pomijamy ruch myszy — za dużo linii
        print(event)
    if event.type == pygame.QUIT:
        pygame.quit(); raise SystemExit

Uruchom. Naciskaj klawisze, klikaj, przewiń kółkiem — obserwuj terminal. Po eksperymencie usuń print.

Odpowiedź
Zobaczysz m.in. KeyDown i KeyUp — naciśnięcie i puszczenie to dwa osobne zdarzenia — oraz MouseButtonDown i MouseWheel. Każde zdarzenie niesie dodatkowe dane: kod klawisza, pozycję kursora, modyfikatory. Pygame rejestruje wszystko — a Twój kod wybiera z tego strumienia tylko to, na co chce reagować. W Snake: QUIT i KEYDOWN, resztę ignorujesz.
✏️ Zadania samodzielne
  1. Dodaj zamykanie gry także klawiszem Q (stała pygame.K_q) — obok Escape. Przetestuj oba.
  2. Przenieś blok RYSOWANIA przed pętlę zdarzeń (na początek while True). Uruchom — działa? Dlaczego w pustym oknie nie widać różnicy i kiedy ten błąd stałby się widoczny? Przywróć poprawną kolejność.
  3. Własnymi słowami wyjaśnij komuś, czym jest game loop i jakie ma trzy fazy. Możesz użyć analogii z flip-bookiem lub wymyślić własną.
✔️ Po T02 powinieneś umieć:
  • Wymienić trzy fazy game loop i wyjaśnić, dlaczego ich kolejności nie wolno zmieniać
  • Wyjaśnić, dlaczego faza ZDARZENIA musi wykonać się w każdej klatce — nawet gdy LOGIKA jest pusta
  • Obsłużyć naciśnięcie konkretnego klawisza — wyjaśnić różnicę między event.type a event.key
  • Wskazać w swoim kodzie, gdzie trafi ruch węża, a gdzie jego rysowanie
Pętla ma strukturę, gra słucha klawiatury. Ale została rzecz, którą zapowiadaliśmy od T01: pętla wciąż kręci się tak szybko, jak pozwala procesor — na jednym komputerze tysiąc obiegów na sekundę, na innym sto. Ten sam kod, różna prędkość gry. W T03 dodasz Clock i FPS — metronom, który sprawi, że Snake będzie działał dokładnie tak samo na każdym sprzęcie.

Wizualnie wciąż to samo okno — wiem, niecierpliwisz się. Jeszcze chwila przygotowań — czas (T03), złożenie silnika (S01) i układ współrzędnych (T04) — a w T05 na planszy wyrośnie pierwszy kwadrat: głowa węża.
🧠 Sprawdź wiedzę — T02
T03

Clock, FPS i pełna pętla

Wyobraź sobie zespół muzyczny bez perkusisty — każdy gra we własnym tempie i utwór się rozjeżdża. Dlatego muzycy ćwiczą z metronomem: urządzeniem, które tyka idealnie równo i wszystkim narzuca wspólne tempo. pygame.time.Clock() to metronom Twojej gry. Wywołanie zegar.tick(8) mówi: „poczekaj tyle, żeby cała klatka trwała dokładnie jedną ósmą sekundy”. Bez metronomu pętla biega ile zdąży — na każdym komputerze inaczej. Z metronomem gra tyka zawsze w tym samym tempie, wszędzie.

T02 skończył się obietnicą: rozwiążemy problem pętli, która kręci się tak szybko, jak pozwala procesor. Czas zrozumieć, dlaczego to w ogóle jest problem. Wyobraź sobie, że wąż przesuwa się o jedną kratkę w każdym obiegu pętli. Na szybkim komputerze pętla wykonuje się tysiąc razy na sekundę — wąż przelatuje przez całą planszę w mgnieniu oka, gra jest nie do ogrania. Na słabym laptopie te same instrukcje wykonują się sto razy na sekundę — wąż pełza dziesięć razy wolniej. Ten sam kod, dwie zupełnie różne gry. Tak być nie może: prędkość gry musi zależeć od Ciebie, nie od sprzętu gracza.

Rozwiązaniem jest miara FPS — frames per second, czyli klatki na sekundę. To liczba mówiąca, ile pełnych obiegów pętli (a więc ile odrysowań ekranu) wykonuje się w ciągu sekundy. Ty ją ustalasz, a zegar Pygame pilnuje, żeby była dotrzymana. Dla Snake wybierzemy FPS = 8 — wąż porusza się skokowo, kratka po kratce, więc 8 ruchów na sekundę daje graczowi około 125 milisekund na reakcję przed kolejnym skokiem. Wystarczająco szybko, żeby trzymać napięcie; wystarczająco wolno, żeby dało się grać.

Mechanizm składa się z dwóch części. Przed pętlą tworzysz zegar — raz: zegar = pygame.time.Clock(). A na samym końcu każdej klatki wywołujesz zegar.tick(FPS) — i tu jest sedno: tick mierzy, ile czasu zajęła bieżąca klatka, i czeka dokładnie tyle, żeby cała trwała 1/FPS sekundy. Szybki komputer poczeka dłużej, wolny krócej — efekt końcowy identyczny na obu.

⌨️ Krok 0 — Otwórz kod z T02
Otwórz snake.py z trójfazową pętlą i obsługą Escape z T02. Dodasz trzy linie: stałą FPS = 8 do bloku stałych, utworzenie zegara przed pętlą i zegar.tick(FPS) na końcu pętli.

Zaktualizuj swój kod do tej postaci — trzy nowe linie są oznaczone komentarzami:

import pygame
pygame.init()

SZEROKOSC = 600
WYSOKOSC  = 600
FPS       = 8          # klatki na sekundę — prędkość gry
KOLOR_TLO = (15, 25, 15)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
zegar = pygame.time.Clock()            # tworzymy metronom

while True:
    # ─── ZDARZENIA ────────────────────────────────────
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # ─── LOGIKA ───────────────────────────────────────

    # ─── RYSOWANIE ────────────────────────────────────
    ekran.fill(KOLOR_TLO)              # zamaluj cały ekran
    pygame.display.flip()              # pokaż gotową klatkę
    zegar.tick(FPS)                    # czekaj do końca klatki
> python snake.py
okno działa z kontrolowanym czasem — 8 klatek/s

Okno wygląda identycznie jak po T02 — i znów o to chodzi. Różnica jest niewidoczna, ale fundamentalna: procesor nie pracuje już na pełnych obrotach renderując tysiące zbędnych klatek, a tempo gry jest w Twoich rękach. Gdy w T06 wąż zacznie się ruszać, jedna zmiana stałej FPS będzie zmieniała prędkość całej gry. Tę właściwość wykorzystasz zresztą na końcu kursu — w finalnej wersji Snake FPS będzie rósł z każdym zjedzonym jabłkiem, podnosząc poziom trudności.

🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): Gdzie w pętli powinno stać zegar.tick(FPS) — na początku czy na końcu klatki?
Odpowiedź
Na końcu — po display.flip(). To standardowe miejsce: tick() mierzy czas od swojego poprzedniego wywołania i dośpia tak, żeby cała klatka (zdarzenia → logika → rysowanie) trwała 1/FPS sekundy. Wołamy go, gdy klatka jest już gotowa.

Zostały dwie linie, które piszesz od T01, ale których jeszcze dokładnie nie omówiliśmy — a w tej lekcji domykamy całą pętlę, więc czas to nadrobić. Chodzi o parę ekran.fill() i pygame.display.flip().

ekran.fill(kolor) zamalowuje cały ekran jednym kolorem. Po co, skoro za chwilę i tak wszystko rysujesz? Bo rysowanie w Pygame nie zna pojęcia „przesuń obiekt” — każda klatka maluje wszystko od zera. Jeśli nie wyczyścisz ekranu, nowa klatka namaluje się na poprzedniej: wąż na pozycji nowej i jednocześnie wszystkie jego stare pozycje. Programiści nazywają to ghost effect — ekran zapełnia się „duchami” poprzednich klatek. Dlatego fill() zawsze otwiera fazę RYSOWANIA: czysta kartka, dopiero potem obiekty.

pygame.display.flip() rozwiązuje inny problem. Wszystko, co rysujesz — fill(), prostokąty, tekst — trafia najpierw do niewidocznego bufora w pamięci, nie prosto na monitor. Gdyby gracz widział rysowanie na żywo, ekran migotałby niedokończonymi klatkami: samo tło, tło z połową węża, wreszcie całość. flip() czeka, aż klatka jest kompletna, i dopiero wtedy pokazuje ją na monitorze — jednym ruchem, jak podniesienie kurtyny. Bez fill() masz duchy; bez flip() nie zobaczysz nigdy nic.

Zakomentuj na próbę linię ekran.fill(KOLOR_TLO) i uruchom — na razie okno wygląda tak samo, bo nic się jeszcze nie rusza. Ale zapamiętaj objaw na przyszłość: gdy w T06 wąż zacznie się poruszać i zobaczysz na planszy smugę zamiast węża — to znak, że zabrakło fill(). Każda klatka rysuje się wtedy na poprzedniej i stare pozycje nigdy nie znikają. Przywróć linię, zanim pójdziesz dalej.

Twoja pętla jest kompletna: zdarzenia, logika, rysowanie, kontrola czasu. Spójrz na nią jeszcze raz w formie diagramu — od startu programu do zamknięcia. Każdy element już znasz; teraz zobaczysz je wszystkie naraz, w jednym przepływie:

flowchart TD S([Start]) --> I[pygame.init + set_mode + Clock] I --> L{while True} L --> E[ZDARZENIA
event.get] E --> Q{QUIT?} Q -->|Tak| X([pygame.quit
raise SystemExit]) Q -->|Nie| G[LOGIKA
ruch / kolizje] G --> R[RYSOWANIE
ekran.fill + flip] R --> T[zegar.tick FPS] T --> L
📄 Cały kod snake.py — snapshot (to co napiszesz w S01)
import pygame
pygame.init()

SZEROKOSC = 600
WYSOKOSC  = 600
FPS       = 8
KOLOR_TLO = (15, 25, 15)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
zegar = pygame.time.Clock()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    ekran.fill(KOLOR_TLO)
    pygame.display.flip()
    zegar.tick(FPS)
🔍 Czemu FPS = 8? Dlaczego nie 60?

Snake to gra skokowa — wąż przesuwa się o jeden segment naraz. Przy 60 FPS wąż przesuwałby się 60 razy na sekundę — za szybko na reakcję. 8 FPS to standard dla klasycznego snake: masz ~125ms na naciśnięcie strzałki przed następnym ruchem. W T06 zobaczysz, jak zmiana FPS wpływa na odczucie gry.

🧪 Eksperyment — ile klatek naprawdę wyciąga Twój komputer?

Zegar potrafi nie tylko czekać — umie też zmierzyć, ile klatek na sekundę faktycznie osiąga Twoja pętla, metodą zegar.get_fps(). Dodaj na końcu pętli, tuż po zegar.tick(FPS), jedną linię:

    print(zegar.get_fps())

Uruchom i poczekaj sekundę. Na samym początku zobaczysz 0.0 — to normalne: get_fps() liczy średnią z ostatnich klatek i potrzebuje kilku, żeby się rozpędzić. Po chwili liczba ustabilizuje się blisko 8 — bo tick(8) trzyma tempo w ryzach. Jeśli widzisz tylko 0, daj programowi parę sekund — nie zamykaj okna za szybko.

Teraz najciekawsze — zdejmij hamulec i zobacz prawdziwą moc swojej maszyny. Zmień tymczasowo stałą na bardzo dużą wartość:

FPS = 5000        # tymczasowo — praktycznie bez limitu

Uruchom ponownie. Przy tick(5000) zegar prawie nie czeka, więc pętla biega na maksa, a terminal pokaże, ile klatek Twój komputer robi NAPRAWDĘ — często 2000, 3000, a nawet ponad 5000 na sekundę. To ta sama pętla, tylko bez metronomu. Po eksperymencie usuń print i przywróć FPS = 8.

Odpowiedź
Przy FPS = 8 terminal po rozgrzewce pokazuje ~8 — zegar dotrzymuje słowa niezależnie od mocy komputera (te pierwsze 0.0 to tylko budowanie średniej). Przy FPS = 5000 hamulec znika i widzisz, ile klatek maszyna wyciąga sama z siebie — setki lub tysiące, na każdym komputerze inaczej. Właśnie tę niekontrolowaną prędkość tick() trzyma w ryzach, żeby gra działała identycznie na każdym sprzęcie.
✏️ Zadania samodzielne
  1. Zmień FPS na 1 i uruchom. Klikaj X i obserwuj: zamknięcie okna potrafi zająć prawie sekundę. Wyjaśnij, dlaczego — co robi pętla między Twoim kliknięciem a obsłużeniem zdarzenia? Przywróć FPS = 8.
  2. Przenieś zegar.tick(FPS) na sam początek pętli (przed zdarzenia). Uruchom — gra działa. Dlaczego mimo to konwencja każe trzymać tick na końcu? Wróć do poprawnej wersji.
  3. Własnymi słowami wyjaśnij różnicę między ekran.fill() a pygame.display.flip() — i co by się stało, gdyby zabrakło każdego z nich.
✔️ Po T03 powinieneś umieć:
  • Wyjaśnić, dlaczego pętla bez kontroli czasu daje różną prędkość gry na różnych komputerach
  • Stworzyć zegar i użyć zegar.tick(FPS) — i wyjaśnić, jak tick wyrównuje czas klatki
  • Wyjaśnić, po co jest fill() (ghost effect) i po co flip() (bufor i migotanie)
  • Prześledzić na diagramie pełny przebieg jednej klatki — od zdarzeń do tick
Pętla jest kompletna: trzy fazy, obsługa klawiatury, kontrolowany czas. To dokładnie ten fundament, na którym stoi każda gra Pygame. W S01 złożysz wszystko w jeden czysty, uporządkowany plik — wzorcowy szkielet, do którego od T05 zacznie wchodzić właściwa gra: najpierw wąż na planszy, potem ruch, sterowanie i kolizje.

To ostatnia pusta plansza. Następna lekcja (S01) składa silnik, a niedługo potem — w T05 — zobaczysz na planszy pierwszy kwadrat, głowę węża. Cierpliwość się opłaci: rozumiesz teraz każdą linię, więc reszta pójdzie szybko.
🧠 Sprawdź wiedzę — T03
S01

Pierwsze działające okno

🎯 Misja S01
Napisz kompletny program, który otwiera okno 600×600 z ciemnozielonym tłem, poprawnie obsługuje zamknięcie i działa w tempie 8 klatek na sekundę. To fundament gry — każda kolejna lekcja buduje na tym kodzie.

Przez trzy lekcje teorii budowałeś ten kod kawałek po kawałku: w T01 import, stałe i okno, w T02 trójfazową pętlę z obsługą klawiatury, w T03 zegar i kontrolę czasu. Teraz inaczej — napiszesz wszystko od zera, w jednym podejściu, bez podglądania poprzednich lekcji. Po co, skoro ten kod już masz? Bo między „rozumiem, gdy czytam” a „umiem napisać sam” jest przepaść, którą zasypuje tylko samodzielne pisanie. Ten szkielet to początek każdej gry Pygame, jaką kiedykolwiek stworzysz — po S01 masz go mieć w palcach.

Spróbuj najpierw z pamięci: stałe, inicjalizacja, pętla z trzema fazami, tick na końcu. Dopiero gdy utkniesz, spójrz na wzorzec poniżej. Jeśli przepiszesz całość bez zrozumienia — oszukasz tylko siebie.

⌨️ Krok 0 — Wyczyść snake.py
Otwórz VS Code → swój folder projektu → snake.py. Usuń dotychczasową zawartość — piszesz od zera, na czysto.
Widok gry — S01 Efekt: puste okno 600×600 z ciemnozielonym tłem i tytułem na pasku. Żadnych elementów — tylko tło.
Gdy uruchomisz, zobaczysz ciemnozielone okno i zamkniesz je bez błędów — misja zaliczona.

Wzorzec, do którego porównasz swój kod. Zwróć uwagę na komentarze dzielące plik na sekcje — STAŁE, INICJALIZACJA, GAME LOOP — od teraz każdy kod w tym kursie będzie tak zorganizowany:

👁 Wzorzec do porównania — otwórz dopiero PO napisaniu swojej wersji
import pygame
pygame.init()

# ─── STAŁE ────────────────────────────────────────────
SZEROKOSC = 600
WYSOKOSC  = 600
FPS       = 8
KOLOR_TLO = (15, 25, 15)   # ciemnozielony

# ─── INICJALIZACJA ────────────────────────────────────
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — S01: Okno")
zegar = pygame.time.Clock()

# ─── GAME LOOP ────────────────────────────────────────
while True:
    # ZDARZENIA
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # LOGIKA                            # ← TUTAJ będziesz dodawał logikę gry

    # RYSOWANIE
    ekran.fill(KOLOR_TLO)
    pygame.display.flip()
    zegar.tick(FPS)
> python snake.py
ciemnozielone okno z tytułem "SNAKE — S01: Okno"
kliknij X żeby zamknąć — powinno działać

Porównaj swój kod ze wzorcem linia po linii. Jeśli coś napisałeś inaczej, ale działa — świetnie, ważna jest struktura, nie identyczność co do znaku. Zwróć jeszcze uwagę na komentarz # ← TUTAJ w sekcji LOGIKA: dziś to puste miejsce, ale za kilka lekcji będzie tam ruch węża, kolizje i zjadanie jabłek — kilkadziesiąt linii. Cała gra wyrośnie wewnątrz szkieletu, który właśnie napisałeś.

Okno się nie otwiera lub natychmiast zamyka? Najpierw przeczytaj komunikat w terminalu — on zawsze mówi, co poszło nie tak. Czerwony ModuleNotFoundError: No module named 'pygame' oznacza brak zainstalowanej biblioteki — wpisz pip install pygame i uruchom ponownie. SyntaxError ze wskazaniem numeru linii to literówka — porównaj tę linię ze wzorcem, najczęściej brakuje dwukropka albo nawiasu. A jeśli okno otwiera się i od razu znika bez błędu — sprawdź, czy pętla while True na pewno jest w pliku i czy kod rysowania jest wcięty do jej wnętrza.
✔️ Zanim przejdziesz dalej — sprawdź:
  • Okno otwiera się po python snake.py
  • Tytuł paska to „SNAKE — S01: Okno”
  • Tło jest ciemnozielone (nie czarne)
  • Kliknięcie X zamyka okno bez błędów w terminalu
  • Stałe SZEROKOSC, WYSOKOSC, FPS, KOLOR_TLO są zdefiniowane przed game loopem
  • Napisałeś przynajmniej część kodu z pamięci, nie przepisując wzorca linia po linii
Właśnie zbudowałeś fundament gry Snake — własnymi rękami, od pustego pliku do działającego programu! To ten sam szkielet, który otwiera profesjonalne gry Pygame, tylko jeszcze bez zawartości. Wiem — to czwarte puste okno z rzędu i pewnie myślisz „kiedy w końcu coś się ruszy?”. Dobra wiadomość: to było ostatnie puste okno. Już w następnej lekcji (T04) postawisz pierwsze kwadraty na planszy i poznasz układ współrzędnych, a w T05 wyrośnie głowa węża — i ruszy lekcję później (T06). Od teraz każda lekcja coś dokłada na ekran. Wszystko zaczyna się tutaj. 🎉
🗺️ Koniec pustych okien — oto co dalej
Okno (zrobione)
✓ okno
Głowa węża
▶ TERAZ
głowa
Ruchomy wąż
ruch →
T04

Układ współrzędnych: gdzie na ekranie

Wyobraź sobie, że dyktujesz koledze, gdzie ma postawić kropkę na kartce. „Trzy w prawo, dwa w dół” — ale od którego punktu zaczyna liczyć i w którą stronę? Bez umowy co do startu i kierunków kropka wyląduje gdzie indziej, niż chciałeś. Komputer ma dokładnie taką umowę dla całego okna: każdy punkt ma swój adres złożony z dwóch liczb, które mówią, jak daleko w prawo i jak daleko w dół. W tej lekcji poznasz tę umowę co do jednego piksela — bo dopóki jej nie znasz, każde „narysuj tutaj” jest zgadywanką.

W S01 zbudowałeś działające okno — pustą planszę o wymiarach 600 na 600 pikseli. Piksel to najmniejszy punkt, jaki ekran potrafi zapalić, a Twoje okno to krata 600 na 600 takich punktów. Żeby cokolwiek na nim narysować, musisz umieć wskazać konkretny punkt, i właśnie do tego służy układ współrzędnych.

Każdy punkt opisują dwie liczby. Pierwsza z nich, x, mówi, jak daleko w prawo; druga, y, jak daleko w dół. Punkt (0, 0) leży w lewym górnym rogu okna. I tu czeka pułapka, na której potyka się większość początkujących twórców gier: oś y rośnie w dół, odwrotnie niż na matematyce, gdzie y rośnie do góry. Dlaczego akurat tak? Bo ekrany od zawsze „kreślą” obraz od góry do dołu, linia po linii, trochę jak czytanie kartki. Dlatego y=0 to górna krawędź okna, a y=600 to sam dół.

(0,0) x=600 y=600 (300,300) y ↓ x → x rośnie → y rośnie ↓ (0,0) = lewy górny róg x = 300 y = 300 → środek okna ⚠ y rośnie w DÓŁ (nie w górę!)

Spójrz na schemat. Czerwony punkt to przykładowa pozycja (300, 300) — w połowie szerokości i w połowie wysokości, czyli środek okna. Skoro umiesz już wskazać punkt, potrzebujesz narzędzia, które coś w nim postawi. Tym narzędziem jest pygame.draw.rect. Przyjrzyj się jego budowie, argument po argumencie:

pygame.draw.rect(
    ekran,              # gdzie rysujemy — Twoj ekran z T01
    (255, 0, 0),        # kolor jako trojka RGB (tu czysta czerwien)
    (300, 300, 20, 20)  # (x, y, szerokosc, wysokosc) — pozycja i rozmiar
)

Trzy argumenty, jeden po drugim: powierzchnia, kolor i czwórka liczb. Kolor podajemy jako trzy liczby od 0 do 255 — pamiętasz z T01, że mówią one, ile w nim czerwieni, zieleni i błękitu. Dlatego (255, 0, 0) to czysta czerwień, a (0, 0, 255) czysty błękit. Z czwórki liczb dwie pierwsze to pozycja, dwie kolejne to rozmiar. Jest jednak szczegół, który zaraz Cię zaskoczy: punkt (x, y) wyznacza lewy górny róg prostokąta, a nie jego środek. Prostokąt rozrasta się od tego rogu w prawo i w dół. Dlatego znacznik o boku 20 pikseli musi zaczynać się najpóźniej na 580: dopiero wtedy zmieści się tuż przy prawej krawędzi (580 + 20 = 600, czyli dokładnie krawędź okna).

🎨 Nie pamiętasz, jak działa RGB z T01? Kliknij, żeby przypomnieć
Kolor w Pygame to trójka liczb (R, G, B) — ile w nim czerwieni (Red), zieleni (Green) i błękitu (Blue). Każda liczba ma jasność od 0 (dioda wyłączona) do 255 (pełna moc). Stąd (0, 0, 0) to czarny, bo żadna dioda nie świeci; (255, 255, 255) to biały, bo wszystkie na maksimum; (255, 0, 0) to czysta czerwień, a (0, 0, 255) czysty błękit. Tło planszy Snake, (15, 25, 15), to bardzo ciemna zieleń — odrobina każdego koloru z przewagą zielonego.
⌨️ Krok 0 — Otwórz snake.py z kodem z S01
Otwórz snake.py — masz w nim szkielet z S01: stałe, inicjalizację i trójfazowy game loop. W tej lekcji nie dokładasz jeszcze nic na stałe — tylko tymczasowo, na własne oczy, sprawdzisz układ współrzędnych.

Dodaj tymczasowo do sekcji RYSOWANIE (po ekran.fill(), a przed flip()) cztery znaczniki — po jednym w każdym rogu okna. Zanim uruchomisz, postaw hipotezę: który kolor wyląduje w którym rogu?

    # Tymczasowo — test układu współrzędnych (usuń po lekcji)
    pygame.draw.rect(ekran, (255, 0, 0),   (0, 0, 20, 20))       # (0,0)   lewy górny
    pygame.draw.rect(ekran, (0, 0, 255),   (580, 580, 20, 20))   # (580,580) prawy dolny
    pygame.draw.rect(ekran, (255, 255, 0), (0, 580, 20, 20))     # (0,580) lewy dolny
    pygame.draw.rect(ekran, (255, 0, 255), (580, 0, 20, 20))     # (580,0) prawy górny
> python snake.py
cztery kolorowe kwadraty w czterech rogach okna

Sprawdź swoje przewidywania. Czerwony usiadł w lewym górnym rogu, bo (0, 0) to początek układu. Niebieski na (580, 580) trafił w prawy dolny — oba duże, więc daleko w prawo i daleko w dół. Żółty na (0, 580) wylądował w lewym dolnym rogu, bo zerowe x trzyma go przy lewej krawędzi, a duże y spycha na sam dół. Magenta na (580, 0) zajął prawy górny róg — odwrotnie niż żółty: duże x niesie go w prawo, a zerowe y trzyma u góry. Jeśli żółty Cię zaskoczył, to znak, że intuicja matematyczna „duże y znaczy wysoko” wciąż w Tobie siedzi — w Pygame jest dokładnie odwrotnie. Te znaczniki są tylko po to, żeby zobaczyć współrzędne na żywo; usuń je na koniec, bo następną lekcję zaczynamy na czystym oknie.

Zwróć uwagę, jak wiele dało się zrobić jednym narzędziem: cztery wywołania draw.rect z różnymi liczbami postawiły cztery kwadraty w czterech różnych miejscach. Za każdym razem to dwie pierwsze liczby — x i y — decydowały, gdzie w oknie ląduje kwadrat, a reszta była taka sama. Opanowanie tej jednej zależności, że pozycja to para liczb, którą sam wybierasz, to połowa całego rysowania w grach.

Znacznik „znika” częściowo albo całkiem przy krawędzi? To nie błąd składni — Pygame nie protestuje, gdy rysujesz poza oknem, po prostu przycina niewidoczną część. Mechanizm jest prosty: widać tylko to, co mieści się w przedziale od 0 do 600. Przy x = 590 i boku 20 ostatnie 10 pikseli wystaje poza okno i ich nie zobaczysz. Zasada na przyszłość: jeśli kwadrat ma być w całości widoczny, x + bok nie może przekroczyć 600.
🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): Co narysuje pygame.draw.rect(ekran, (255,0,0), (0, 0, 600, 600))?
Odpowiedź
Czerwony prostokąt na CAŁY ekran: start w (0, 0), rozmiar 600×600 — dokładnie wymiary okna. Wywołany po fill() zakryje tło; wywołany przed — sam zostanie zakryty. Czyli fill() to w gruncie rzeczy wygodny skrót dla „prostokąt na cały ekran”.
🧪 Eksperyment — wyceluj w róg

Hipoteza na start: jakie wartości x i y postawią znacznik w prawym górnym rogu okna, w całości widoczny?

  1. Zapisz swoją odpowiedź, zanim sprawdzisz.
  2. Dodaj znacznik z tymi wartościami i uruchom. Trafiłeś?
  3. Teraz spróbuj x = 595 przy boku 20 — 595 + 20 = 615. Co zobaczyłeś przy prawej krawędzi i dlaczego?
Odpowiedź
Prawy górny róg to duże x i małe y: (580, 0) (580 to ostatnia pozycja, w której bok 20 jeszcze mieści się w oknie: 600 − 20). Przy x = 595 znacznik jest przycięty — widać tylko 5 z 20 pikseli, bo reszta wychodzi poza okno. Pygame rysuje bez błędu, po prostu obcina to, co nie mieści się w kadrze.
✏️ Zadania samodzielne
  1. Postaw znacznik dokładnie na środku górnej krawędzi okna — najpierw przelicz, jakie powinno być x i y, potem sprawdź uruchamiając.
  2. Zepsuj celowo: ustaw jednemu znacznikowi y = -10. Uruchom i opisz, dlaczego widać tylko jego kawałek. Przywróć poprawną wartość.
  3. Usuń wszystkie cztery tymczasowe znaczniki — okno ma wrócić do pustego, samego tła.
  4. Wyjaśnij własnymi słowami, gdzie leży (0, 0) i w którą stronę rośnie y — bez zaglądania do schematu.
✔️ Po T04 powinieneś umieć:
  • Wskazać (0, 0) i kierunki osi — i wyjaśnić, czym układ ekranowy różni się od matematycznego
  • Przewidzieć, w którym miejscu okna wyląduje prostokąt o zadanych (x, y), zanim uruchomisz kod
  • Wyjaśnić, dlaczego (x, y) wyznacza lewy górny róg prostokąta, a nie jego środek
  • Wyjaśnić, dlaczego znacznik o boku 20 musi zaczynać się najpóźniej na 580, by zmieścił się w całości
Umiesz już trafić w dowolny piksel — ale okno 600 na 600 to 360 tysięcy punktów, a wąż nie potrzebuje aż takiej precyzji: ma skakać po równych polach. W T05 nałożymy na te piksele siatkę — pogrupujemy je w pola po 20 pikseli i narysujesz pierwszą głowę węża jako kwadrat wypełniający jedno pole. Piksele zejdą wtedy na drugi plan, a Ty zaczniesz myśleć polami.
🧠 Sprawdź wiedzę — T04
T05

Siatka: z pikseli w pola

Wyobraź sobie, że na gładką białą kartkę kładziesz przezroczystą folię w drobną kratkę — taką jak w zeszycie do matematyki. Nagle ta sama kartka czyta się inaczej. Zamiast mówić „postaw kropkę gdzieś w połowie”, mówisz „trzecie pole w prawo, drugie w dół” — i kropka ląduje dokładnie tam, bez mierzenia linijką. Tak samo działa szachownica nałożona na obraz: pola same podpowiadają, gdzie co stoi. Za chwilę nałożysz taką siatkę na swoją planszę z pikseli. Dzięki polom wąż będzie trafiał idealnie, a Ty przestaniesz liczyć setki pikseli — zaczniesz myśleć całymi polami.

W T04 nauczyłeś się trafiać w dowolny piksel planszy. To potężna umiejętność, ale dla węża zbyt dokładna: okno ma 600 na 600 pikseli, czyli 360 tysięcy osobnych punktów, a wąż nie potrzebuje takiej precyzji. On ma skakać po równych, sąsiadujących polach, a nie celować w pojedyncze punkty.

Dlatego grupujemy piksele w pola. Umawiamy się, że jedno pole to kwadrat o boku 20 pikseli, i zapisujemy tę umowę w stałej KRATKA = 20. Skoro plansza ma 600 pikseli szerokości, a pole 20, to mieści się ich 600 ÷ 20 = 30 w jednym rzędzie. Tyle samo w pionie — razem siatka 30 × 30, czyli 900 pól. Od tej chwili myślimy polami, a piksele zostają tylko przelicznikiem w tle.

wąż = 1 pole (0,0) 30 × 30 pól 1 pole = 20 px → KRATKA = 20 Każde oczko = jedno pole. Zielone = głowa węża. Wąż zawsze wypełnia całe pole — nigdy jego kawałek.

Jak z numeru pola wyliczyć piksel? Reguła jest jedna i prosta. Pole o numerze (kolumna k, wiersz r) zaczyna się w pikselu (k×20, r×20). Pole (15, 15) zaczyna się więc w pikselu (300, 300) — dokładnie w środku planszy. Pole (0, 0) to piksel (0, 0), czyli lewy górny róg — z T04 wiesz, gdzie on jest i jak trafić w piksel.

Ten sam kwadrat możesz postawić w różnym polu — wystarczy zmienić numer pola, a piksel (x, y) policzy się sam. Inne pole to po prostu inne (x, y). I jedna rzecz nie zmienia się względem T04: (x, y) to wciąż lewy górny róg kwadratu. Dlatego głowa siedzi swoim rogiem dokładnie w rogu pola, a stamtąd wypełnia je w prawo i w dół.

⌨️ Krok 0 — Otwórz snake.py z kodem z T04
Otwórz snake.py z T04 — masz w nim czysty szkielet: stałe, inicjalizację i trójfazowy game loop, ze znacznikami testowymi już usuniętymi. W T04 dorysowywałeś kwadraty tylko tymczasowo. Teraz dodasz pierwszy obiekt gry na stałe: głowę węża.

Czas postawić pierwszy stały element gry. Dodasz dwie nowe stałe — KRATKA i KOLOR_GLOWA — ustawisz pozycję głowy na środku planszy i dorzucisz jedną linię draw.rect w sekcji RYSOWANIE. To pierwszy obiekt, który zostaje na ekranie na dobre, a nie tylko na czas testu.

import pygame
pygame.init()

SZEROKOSC   = 600
WYSOKOSC    = 600
KRATKA      = 20          # rozmiar jednego pola siatki w pikselach
FPS         = 8
KOLOR_TLO   = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)   # jasny zielony

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T05: glowa")
zegar = pygame.time.Clock()

# pozycja startowa glowy — srodek planszy (pole 15,15 = piksel 300,300)
glowa_x = 300
glowa_y = 300

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    ekran.fill(KOLOR_TLO)
    pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
    pygame.display.flip()
    zegar.tick(FPS)
> python snake.py
zielone okno z jasnozielonym kwadratem 18×18 na środku

Na środku planszy siedzi jasnozielony kwadrat — pierwszy stały obiekt Twojej gry. Zatrzymaj się na chwilę nad tym, co jest tu zmienną, a co stałą. glowa_x i glowa_y to zmienne, bo opisują stan, który będzie się zmieniał — w T06 głowa ruszy z miejsca i te liczby zaczną rosnąć. KRATKA i kolory to stałe, czyli konfiguracja, która się nie zmienia w trakcie gry.

Głowa stoi w pikselu (300, 300), więc — jak już wiesz z przelicznika — siedzi w polu (15, 15), w samym środku siatki 30 × 30. Rogiem trzyma się rogu pola, a resztę pola wypełnia w prawo i w dół. To jest dokładnie ten obiekt, którym za chwilę zaczniesz sterować.

Pewnie zauważyłeś dziwny szczegół w kodzie — kwadrat ma bok KRATKA - 2, czyli 18, a nie pełne 20. Po co ta dwójka? Spójrz na tę jedną linię z osobna:

pygame.draw.rect(
    ekran,
    KOLOR_GLOWA,        # kolor glowy (zielony)
    (glowa_x, glowa_y,  # x, y = lewy gorny rog pola (z T04)
     KRATKA - 2,        # szerokosc: 20 - 2 = 18 px
     KRATKA - 2)        # wysokosc: tak samo, 18 px
)
# te 2 px marginesu zostawiaja szczeline tla -> widac linie miedzy polami

Chodzi właśnie o tę widoczną linię między polami. Pole ma 20 pikseli, ale kwadrat zajmuje tylko 18 — zostają 2 piksele marginesu, przez które prześwituje ciemne tło. Przy jednym kwadracie różnicy prawie nie widać, ale gdy wąż urośnie i pól będzie wiele obok siebie, te szczeliny tła narysują cienkie linie siatki między segmentami. Dzięki temu wąż wygląda jak łańcuch osobnych ogniw, a nie jeden zlany pasek. Spójrz na schemat — od razu zobaczysz różnicę.

bok = KRATKA (20) segmenty zlewają się w jeden pasek bok = KRATKA - 2 (18) widać linie między polami → wąż jak łańcuch ogniw
Kwadratu w ogóle nie widać? Dwie najczęstsze przyczyny. Pierwsza — draw.rect stoi przed ekran.fill(), więc fill zamalowuje świeżo narysowany kwadrat tłem; rysowanie zawsze idzie po wypełnieniu tła. Druga — KRATKA ma za małą wartość i KRATKA - 2 wyszło 0 albo liczbę ujemną, a kwadrat o boku zero jest niewidoczny. Sprawdź kolejność linii i wartość stałej.
🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): W którym polu siatki siedzi głowa narysowana na pikselu (300, 300)?
Odpowiedź
W polu (15, 15), bo 300 ÷ 20 = 15 — piętnasta kolumna i piętnasty wiersz, czyli sam środek siatki 30 × 30. Przelicznik pole↔piksel działa w obie strony: numer pola razy 20 daje piksel, a piksel przez 20 daje numer pola.
🧪 Eksperyment — po co KRATKA - 2

Hipoteza na start: czy odjęcie tych 2 pikseli w ogóle widać przy jednym kwadracie? A czy zobaczysz różnicę, gdyby pól było więcej?

  1. Zmień oba KRATKA - 2 na samo KRATKA i uruchom. Przy jednym kwadracie różnicy prawie nie ma — zapamiętaj to na potem, gdy segmentów będzie wiele i margines zacznie rysować linie siatki.
  2. Teraz spróbuj KRATKA - 10. Kwadrat robi się mały i zagubiony w polu — za duży margines psuje wygląd.
  3. Wróć do KRATKA - 2 — to złoty środek między „zlanym paskiem” a „zagubioną kropką”.
Odpowiedź
Przy jednym kwadracie odjęcie 2 pikseli ledwie widać — różnica ujawnia się dopiero przy wielu sąsiadujących polach, gdy cienkie szczeliny tła rozdzielają segmenty na osobne ogniwa. KRATKA - 10 zostawia za duży margines: kwadrat ginie w polu. KRATKA - 2 to dobry kompromis.
✏️ Zadania samodzielne
  1. Przenieś głowę do pola (5, 5) — najpierw przelicz na piksele (k×20, r×20), powinno wyjść (100, 100), potem sprawdź uruchamiając.
  2. Dorysuj w sekcji RYSOWANIE drugi kwadrat w innym polu (np. pole (20, 10) = piksel (400, 200)), uruchom, a potem usuń tę linię — okno ma wrócić do samej głowy.
  3. Zepsuj celowo: przenieś ekran.fill(KOLOR_TLO) POD linię draw.rect. Uruchom, opisz, dlaczego głowa zniknęła, i przywróć poprawną kolejność.
  4. Własnymi słowami wyjaśnij komuś, dlaczego glowa_x jest zmienną, a KRATKA stałą — bez zaglądania do lekcji.
✔️ Po T05 powinieneś umieć:
  • Wyjaśnić, czym jest siatka 30 × 30 i po co grupujemy piksele w pola po 20
  • Narysować głowę jako kwadrat w polu jedną linią draw.rect
  • Przeliczyć numer pola na piksele i z powrotem (pole 15 ↔ piksel 300)
  • Wyjaśnić, po co jest KRATKA - 2 i skąd biorą się widoczne linie między polami
Głowa stoi nieruchomo na środku — to dopiero połowa sukcesu. W T06 dodasz dwie liczby kierunku, dx i dy, oraz jedną linię glowa_x += dx, i kwadrat ruszy z miejsca. Już teraz zapamiętaj jedną rzecz z T04: ruch „w górę” to będzie odejmowanie od y, bo oś y rośnie w dół. Za chwilę zobaczysz, jak bardzo to się przyda.
🗺️ Droga do ruchomego węża
Okno (zrobione)
✓ okno
Głowa węża
▶ TERAZ
głowa
Ruchomy wąż
ruch →
🧠 Sprawdź wiedzę — T05
T06

Ruch: dx, dy i +=

Wyobraź sobie nakręcaną zabawkę-żółwia postawioną na podłodze. Nakręcasz, ustawiasz pyszczkiem w stronę drzwi, puszczasz — i żółw maszeruje w tę stronę krok za krokiem, dopóki ktoś go nie obróci. Żółw nie pamięta trasy ani celu. Pamięta tylko jedno: kierunek, w którym jest obrócony. Głowa Twojego węża będzie działać identycznie — dwie liczby, dx i dy, opisują „w którą stronę jest obrócona”, a pętla gry co klatkę robi jeden krok w tym kierunku. Sterowanie wężem to obracanie żółwia: zmiana tych dwóch liczb.

To lekcja, na którą pracowały trzy poprzednie: kwadrat z T05 wreszcie się ruszy, a wszystko sprowadzi się do prostej matematyki na współrzędnych z T04. Kierunek ruchu opiszesz dwiema liczbami: dx i dy — litera „d” jak delta, czyli zmiana. dx mówi, o ile zmienia się x w każdej klatce, a dy — o ile zmienia się y. Zanim ruszymy głową, przypomnij sobie dwa operatory, których zaraz użyjemy — += oraz -=:

# dodawanie:  +=
x = 5
x += 1     # to samo co  x = x + 1   ->  x ma teraz 6

# odejmowanie:  -=
y = 8
y -= 1     # to samo co  y = y - 1   ->  y ma teraz 7

# po prawej moze byc tez ZMIENNA, nie tylko liczba:
krok = 20
x += krok  # to samo co  x = x + krok  ->  x rosnie o 20

Czyli += dodaje do tego, co już jest w zmiennej, a -= odejmuje — to po prostu krótszy zapis tej samej operacji. Dlatego ruch głowy zapiszemy identycznie: glowa_x += dx to dokładnie to samo co glowa_x = glowa_x + dx, tylko zamiast jedynki dodajemy dx. Cały ruch to ta jedna linia powtarzana co klatkę — osobno dla x i osobno dla y.


Zamiast uczyć się czterech kierunków naraz, weźmiemy je po kolei — każdy z osobnym rysunkiem.

1. Kierunek w prawo. Zacznij od najprostszego: ustawiasz dx = +KRATKA (x rośnie o 20) przy dy = 0. Głowa z pola (300, 300) przeskakuje do sąsiedniego (320, 300):

KRATKA = 20      # jedno pole = 20 px (z T05)
dx = +KRATKA     # +20: x rosnie  ->  ruch w prawo
dy = 0           # y bez zmian (pion)

glowa_x += dx    # przesuwasz glowe o dx  ->  i jedziesz!
glowa_y += dy    # dy = 0, wiec pion stoi
start: (300, 300) PRAWO → dx = +KRATKA dy = 0 x: 300 → 320 y: 300 (bez zmian)

2. Kierunek w lewo. Tu jest odwrotnie: skoro x ma maleć, dajesz dx = -KRATKA (z minusem!), wciąż przy dy = 0. Ten sam start, ale głowa cofa się o jedno pole, do (280, 300):

start: (300, 300) ← LEWO dx = -KRATKA dy = 0 x: 300 → 280 y: 300 (bez zmian)

3. Kierunek w dół. Teraz pion — i tu procentuje T04. W dół to dy = +KRATKA przy dx = 0. Uwaga na pułapkę z układu współrzędnych: y rośnie w dół, więc dodawanie do y spycha głowę niżej, do (300, 320):

start: (300, 300) DÓŁ ↓ dy = +KRATKA dx = 0 y: 300 → 320 y rośnie w dół!

4. Kierunek w górę. Góra to lustrzane odbicie dołu: dy = -KRATKA. Skoro y rośnie w dół, to odejmowanie od y unosi głowę do góry, do (300, 280). To klasyczna pułapka „każę w górę, a jedzie w dół” — ale Ty już wiesz, czemu w górę to minus:

start: (300, 300) GÓRA ↑ dy = -KRATKA dx = 0 y: 300 → 280 odejmujemy od y

Teraz, gdy rozumiesz każdy kierunek z osobna, zbierzmy je w jedną ściągę. Zwróć uwagę na prawidłowość: zawsze dokładnie jedna z dwóch liczb jest zerem — to definicja ruchu w klasycznym Snake, w którym wąż sunie poziomo albo pionowo, nigdy na skos.

Kierunek dx dy Dlaczego
prawo →+KRATKA0x rośnie w prawo
lewo ←-KRATKA0x maleje w lewo
dół ↓0+KRATKAy rośnie w dół!
góra ↑0-KRATKAy maleje w górę
⌨️ Krok 0 — Otwórz snake.py z kodem z T05
Otwórz snake.py — głowa na (300, 300), kwadraty kontrolne usunięte. Dodasz dwie zmienne kierunku przed pętlą i dwie linie ruchu w sekcji LOGIKA.

Zaktualizuj kod do tej postaci — nowości to dx, dy przed pętlą oraz sekcja LOGIKA, która po raz pierwszy przestaje być pusta:

import pygame
pygame.init()

SZEROKOSC   = 600
WYSOKOSC    = 600
KRATKA      = 20
FPS         = 8
KOLOR_TLO   = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T06: Ruch")
zegar = pygame.time.Clock()

glowa_x = 300
glowa_y = 300
dx = KRATKA    # startujemy w prawo
dy = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # ─── LOGIKA — przesuń głowę ────────────────────
    glowa_x += dx   # x = x + KRATKA = 20 pikseli w prawo
    glowa_y += dy   # y = y + 0 = bez zmian

    # ─── RYSOWANIE ─────────────────────────────────
    ekran.fill(KOLOR_TLO)
    pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
    pygame.display.flip()
    zegar.tick(FPS)
> python snake.py
zielony kwadrat jedzie w prawo i znika za krawędzią — to OK na razie

Rusza się! Kwadrat startuje ze środka i równym tempem jedzie w prawo — osiem skoków na sekundę, każdy o jedną kratkę. Zatrzymaj się na moment przy tym, co właśnie widzisz: to pierwsza prawdziwa animacja w Twoim kursie. Pamiętasz flip-book z T02? Właśnie patrzysz na niego w akcji — każda klatka rysuje kwadrat w odrobinę innym miejscu, bo między klatkami glowa_x urosło o 20. Iluzja ruchu z nieruchomych obrazków.

Po kilku sekundach kwadrat znika za prawą krawędzią — i to na razie jest w porządku. Głowa przesuwa się bez sprawdzania granic: glowa_x rośnie powyżej 600, a Pygame, jak wiesz z T04, po prostu nie rysuje tego, co poza oknem. Kwadrat „istnieje” dalej — gdzieś na (700, 300), (720, 300)… — tylko go nie widać. Wykrywanie wyjścia poza planszę i kończenie gry to temat T18 i S08. Na razie celowo zostawiamy świat bez ścian.

Kwadrat zamiast jechać — skacze pod lewą krawędź i stoi? Zajrzyj do linii ruchu: glowa_x =+ dx to NIE to samo co glowa_x += dx. Odwrócone znaki Python czyta jako glowa_x = +dx, czyli „przypisz 20” — co klatkę od nowa. Głowa ląduje na x = 20 i już tam zostaje. Błąd jest podstępny, bo nie daje żadnego komunikatu — kolejność znaków sprawdzisz tylko wzrokiem: plus PRZED znakiem równości.
🤔 Spróbuj zgadnąć (nie szkodzi, jeśli jeszcze nie wiesz): Kwadrat zniknął za prawą krawędzią. Jak ustawisz dx i dy, żeby jechał w dół?
Odpowiedź
dx = 0, dy = KRATKA. Pamiętaj z T04: y rośnie W DÓŁ, więc dodatnie dy = ruch w dół.
📄 Snapshot pełnego kodu T06
import pygame
pygame.init()

SZEROKOSC   = 600
WYSOKOSC    = 600
KRATKA      = 20
FPS         = 8
KOLOR_TLO   = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)

ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T06")
zegar = pygame.time.Clock()

glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit
    glowa_x += dx
    glowa_y += dy
    ekran.fill(KOLOR_TLO)
    pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
    pygame.display.flip()
    zegar.tick(FPS)
🔍 Czy można ustawić dx i dy na raz — np. dx=20, dy=20?
Technicznie tak — kwadrat pójdzie po przekątnej. Ale w klasycznym snake wąż porusza się tylko poziomo lub pionowo, nigdy po skosie. Dlatego zawsze jedno z dx, dy jest zerem, a drugie ±KRATKA — i ta reguła wróci w T08, gdy będziemy pisać sterowanie.
🧪 Eksperyment — kierunki i prędkość

Teraz, gdy głowa się rusza, masz dwa pokrętła: kierunek (dx/dy) i tempo (FPS). Sprawdź oba. Najpierw hipoteza: ile kratek na sekundę przejedzie głowa przy FPS=8? A przy FPS=16?

  1. Wypróbuj wszystkie cztery kierunki z tabeli — po jednej zmianie startowych dx/dy na uruchomienie. Za każdym razem przewidź, za którą krawędzią kwadrat zniknie.
  2. Wróć do ruchu w prawo i zmień FPS = 8 na 4, potem na 16. Obserwuj prędkość.
  3. Na koniec przywróć FPS = 8 i kierunek w prawo — to stan wyjściowy do S02.
Odpowiedź
Każda klatka to jeden skok o kratkę, więc prędkość w kratkach na sekundę równa się dokładnie FPS: przy 8 — osiem kratek/s, przy 16 — szesnaście. Dlatego w finalnej grze wystarczy podnosić FPS, żeby wąż przyspieszał — zapamiętaj to, wróci w T25. A kierunek „w górę” (dy = -KRATKA) wyprowadza kwadrat za górną krawędź — ujemne dy zmniejsza y, a małe y to góra ekranu.
✏️ Zadania samodzielne
  1. Ustaw start w lewym górnym rogu (0, 0) i kierunek w dół — kwadrat powinien zjechać wzdłuż lewej krawędzi. Przewidź czas przejazdu całej planszy (30 kratek przy 8 kratkach/s), zmierz i porównaj.
  2. Zepsuj celowo: ustaw dx = KRATKA * 2. Uruchom i opisz, co zmieniło się w ruchu — i dlaczego taki wąż psułby grę opartą na kratkach. Przywróć dx = KRATKA.
  3. Przenieś linie glowa_x += dx i glowa_y += dy POZA pętlę (przed while True). Uruchom. Dlaczego kwadrat stoi? Co to mówi o roli pętli w animacji? Przywróć kod.
  4. Własnymi słowami: jak zwolnić ruch węża dwukrotnie — i to na dwa różne sposoby (jeden przez FPS, drugi bez dotykania FPS)? Który psuje siatkę kratek?
✔️ Po T06 powinieneś umieć:
  • Wyjaśnić, czym są dx i dy — i ustawić je dla każdego z 4 kierunków bez zaglądania do tabeli
  • Wyjaśnić, co robi glowa_x += dx i dlaczego musi być w sekcji LOGIKA wewnątrz pętli
  • Powiązać FPS z prędkością węża: dlaczego prędkość w kratkach/s równa się FPS
  • Wyjaśnić, dlaczego w Snake zawsze jedna z delt jest zerem
Wąż jedzie — ale jak nakręcony żółw: w jedną stronę, do końca świata, bez możliwości skrętu. Zanim dasz graczowi stery, utrwalisz fundament: w S02 napiszesz cały kod ruchomej głowy od zera, z pamięci — stałe, współrzędne, delty, pętla. A zaraz potem T07 odda kierunek w ręce gracza: strzałki zaczną zmieniać dx i dy.
🧠 Sprawdź wiedzę — T06
S02

Ruchoma głowa węża

🎯 Misja S02
Napisz kompletny program z ruchomą głową węża: zielony kwadrat startuje na środku planszy i jedzie w prawo z prędkością 8 kratek na sekundę. Składasz w jedno wszystko z T04, T05 i T06 — współrzędne, siatkę i ruch.

Zasada ta sama co w S01: pisz z pamięci, nie przepisuj. Tym razem masz więcej elementów do złożenia — przejdź je w głowie, zanim dotkniesz klawiatury. Szkielet znasz z S01: stałe, inicjalizacja, trójfazowa pętla. Do tego dochodzą nowe klocki: stała KRATKA i kolor głowy (T05), pozycja startowa na środku (T05), delty kierunku i ruch += w sekcji LOGIKA (T06) oraz draw.rect w RYSOWANIU. Jeśli pamiętasz po co jest każdy klocek, kolejność złoży się sama.

⌨️ Krok 0 — Wyczyść snake.py
Otwórz snake.py i usuń dotychczasową zawartość — piszesz od zera, na czysto. Wzorzec poniżej służy do porównania PO napisaniu własnej wersji.
Widok gry — S02
👁 Wzorzec do porównania — otwórz dopiero PO napisaniu swojej wersji
import pygame
pygame.init()

# ─── STAŁE ────────────────────────────────────────────
SZEROKOSC   = 600
WYSOKOSC    = 600
KRATKA      = 20           # rozmiar jednej kratki w pikselach
FPS         = 8
KOLOR_TLO   = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)

# ─── INICJALIZACJA ────────────────────────────────────
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — S02: Ruchoma głowa")
zegar = pygame.time.Clock()

# ─── POZYCJA STARTOWA ─────────────────────────────────
glowa_x = 300   # środek planszy w poziomie
glowa_y = 300   # środek planszy w pionie

# ─── KIERUNEK RUCHU ───────────────────────────────────
dx = KRATKA    # startujemy w prawo: x rośnie o KRATKA co klatkę
dy = 0         # brak ruchu pionowego      ← TUTAJ zmienisz na skręt (T07)

# ─── GAME LOOP ────────────────────────────────────────
while True:
    # ZDARZENIA
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # LOGIKA — przesuń głowę
    glowa_x += dx
    glowa_y += dy

    # RYSOWANIE
    ekran.fill(KOLOR_TLO)
    pygame.draw.rect(ekran, KOLOR_GLOWA,
        (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
    pygame.display.flip()
    zegar.tick(FPS)
> python snake.py
zielony kwadrat startuje na środku i jedzie w prawo
znika za prawą krawędzią — kolizje dodamy w S08

Porównaj swoją wersję ze wzorcem. Drobne różnice w nazwach czy kolejności stałych nie mają znaczenia — liczy się struktura: stałe na górze, stan gry (pozycja, kierunek) przed pętlą, ruch w LOGICE, rysowanie po fill(). Zwróć uwagę na komentarz ← TUTAJ zmienisz na skręt (T07) przy dy — to dokładnie ten punkt, w którym za dwie lekcje strzałki przejmą kontrolę nad kierunkiem.

Kwadrat stoi w miejscu? Sprawdź, czy glowa_x += dx jest wewnątrz pętli, w sekcji LOGIKA — i czy dx na pewno wynosi KRATKA, a nie 0 (to dokładnie ta usterka, którą sam wywołałeś w zadaniu 3 z T06). Kwadrat mignął i znikł? Pewnie wystartował poza planszą — sprawdź pozycję startową (300, 300). Kwadratu w ogóle nie widać? Klasyka z T04: draw.rect musi stać PO ekran.fill(), inaczej tło go zamalowuje. A jeśli ruch jest, ale „klatkuje” nierówno — upewnij się, że zegar.tick(FPS) nie wypadł z końca pętli.
✔️ Zanim przejdziesz dalej — sprawdź:
  • Zielony kwadrat pojawia się na środku planszy
  • Kwadrat porusza się w prawo równym tempem (8 kratek/s)
  • Okno zamyka się poprawnie klikając X
  • Stałe KRATKA i KOLOR_GLOWA są przed pętlą; pozycja i delty to zmienne
  • Ruch (glowa_x += dx) jest w LOGICE, przed rysowaniem
  • Napisałeś przynajmniej szkielet z pamięci, zaglądając do wzorca tylko przy zacięciu
Właśnie stworzyłeś pierwszą animację — coś naprawdę rusza się na ekranie, a Ty rozumiesz każdy mechanizm, który za tym stoi: pętlę, klatki, współrzędne i delty. Na razie kwadrat jedzie jak automat, bez udziału gracza. To zmieni się w następnym kroku: w T07 nauczysz grę słuchać strzałek, a w S03 wąż po raz pierwszy posłucha Twoich poleceń. 🎉
🏁 Dotarłeś — ruchomy wąż!
Okno (zrobione)
✓ okno
Głowa (zrobione)
✓ głowa
Ruchomy wąż
▶ ruchomy
wąż 🏁
🗺️ Gdzie jesteś? Darmowa część kursu kończy się właśnie tutaj — na ruchomym wężu, którego napisałeś od zera i rozumiesz w całości. Od T07 zaczyna się część płatna: sterowanie strzałkami, rosnące ciało, jabłka i kolizje, ekran Game Over z restartem, a na końcu rekord zapisywany na dysk. Jeśli kurs Ci siadł — kliknij „T07 →”, a zobaczysz, jak odblokować resztę. Twój dotychczasowy postęp zostaje.
🎓

Sprawdzian mistrza — egzamin z całego kursu

Zbudowałeś grę od pustego okna po rekord na dysku — czas domknąć kurs z tarczą. 15 pytań wylosowanych z całej puli (teoria + bonusy). Próg: 80%, czyli 12 z 15. Każde podejście to NOWY zestaw — prób bez limitu.

To nie kartkówka, tylko lustro: pokaże, które lekcje warto odświeżyć. Pytania pojawiają się pojedynczo, po odpowiedzi od razu widzisz poprawną. Wynik poniżej progu? Zanotuj tematy, przy których się potknąłeś, wróć do tych lekcji — i losuj nowy zestaw.