Rozdział 19 · Testowanie

Debug Labs są typowe błędy „Węża”

Każdy błąd tutaj występuje w prawdziwych projektach studenckich — naucz się rozpoznawać objaw, zanim otworzysz debugator.

Mała kolekcja typowych błędów „Wężyka” — każdy z objawem i poprawką. Część z nich widzieliście już wcześniej w rozdziale; tutaj są zebrane jako podręcznik.

Debug Lab 1: Zapomniano screen.tracer(0)
bez_tracer.py
screen = turtle.Screen()
screen.title(„Wąż”)
# tracer(0) nie wywołano
# Игра работает, но каждое движение головы и КАЖДОГО сегмента
# перерисовывается отдельно — заметное мерцание и заметная задержка.
Co widać na ekranie

Bez tracer(0) Turtle odświeża ekran po każdym pojedynczym połączeniu goto() — sekcja 19.2 już wyjaśniła, dlaczego należy wyłączyć automatyczne aktualizacje i robić to ręcznie, dokładnie raz na tick.

Poprawiony kod
s_tracer.py
screen = turtle.Screen()
screen.title(„Wąż”)
screen.tracer(0)
Debug Lab 2: Zapomniałem screen.update() na końcu render()
bez_update.py
def render(self):
    self.head.goto(*self.state.snake[0])
    # ...pozostałe renderowanie...
    # self.screen.update() nie wywołany
# Модель меняется на каждом тике (можно проверить print()),
# но на экране змейка стоит неподвижно.
Co widać na ekranie

P tracer(0) instrukcja screen.update() jest jedynym momentem, w którym zmiany docierają na ekran. Ta sama zasada co w rozdziale 17: zmiana modelu bez wywołania renderowania jest niewidoczna dla użytkownika.

Poprawiony kod
s_update.py
def render(self):
    self.head.goto(*self.state.snake[0])
    # ...pozostałe renderowanie...
    self.screen.update()
Debug Lab 3: Pętla blokująca utrudnia pauzę i prędkość
busy_loop.py
while igrovoj_shag():
    screen.update()
    time.sleep(ZADERZHKA_SEK)   # stała latencja
# pętla decyduje, kiedy dokładnie sprawdzić zdarzenia — pauzę, prędkość
# i restart musi być ręcznie wpleciony w jego ciało
# Игру нельзя поставить на паузу: событие обрабатывается,
# но цикл ничего не проверяет и продолжает крутиться дальше.
# Скорость задана одной константой и по ходу партии не меняется.
Co widać na ekranie

Rozdział 19.13 szczegółowo poruszała ten temat: screen.update() w środku while OBSŁUGUJE Tkinter zdarzenia, w tym naciśnięcia klawiszy — technicznie rzecz biorąc, obsługa pauzy mogłaby działać. Ale sama pętla nie pyta, czy musi się zatrzymać: pauza, prędkość i restart musiałyby być ręcznie sprawdzane przy każdej iteracji. screen.ontimer() jest inna: każdy tick jest osobnym zaplanowanym wywołaniem, a nie iteracją jednej nieskończonej pętli, a decyzja o „wykonaniu następnego ticku lub nie”

Poprawiony kod
ontimer_vmesto_busy.py
def game_tick(self):
    if self.state.status is not GameStatus.RUNNING:
        return
    # ...
    self.screen.ontimer(self.game_tick, self.state.delay_ms)
Debug Lab 4: time.sleep() w cyklu gry
s_sleep.py
def game_tick(self):
    # ...
    time.sleep(self.state.delay_ms / 1000)
    self.game_tick()
# Окно замирает НАВСЕГДА, а не на delay_ms: тик зовёт сам себя
# без условия выхода, поэтому пауз становится бесконечно много,
# а примерно через 1000 вызовов программа падает с RecursionError.
Co widać na ekranie

Błędy są tu dwa. Pierwszy: time.sleep() blokuje CAŁY proces, włącznie z cyklem zdarzeń Tkinter, na którym opiera się Turtle.. game_tick() wywołuje się bezpośrednio, bez żadnego warunku wyjścia, to po prostu nieskończona rekurencja, a stos się wyczerpie, zanim gracz zdąży uzyskać choćby jedną odpowiedź. screen.ontimer() rozwiązuje oba: nic nie blokuje i nie zwiększa stosu — rozdział 19.13 wyjaśnia różnicę szczegółowo.

Poprawiony kod
bez_sleep.py
def game_tick(self):
    # ...
    self.screen.ontimer(self.game_tick, self.state.delay_ms)
Debug Lab 5: dozwolony obrót o 180°
bez_is_reverse.py
def request_direction(self, direction):
    self.state.next_direction = direction   # bez weryfikacji!
# Змейка едет вправо, игрок нажимает Left —
# голова немедленно врезается в первый сегмент собственного тела.
Co widać na ekranie

Bez is_reverse() nic nie stoi na przeszkodzie, by zamienić węża w jego ciało jednym kliknięciem — sekcje 19.4 (i 19.11) wyjaśniały, dlaczego obrót o 180° powinien być zabroniony na poziomie wejścia, a nie tylko przypadkowo.

Poprawiony kod
s_is_reverse.py
def request_direction(self, direction):
    if is_reverse(self.state.direction, direction):
        return
    self.state.next_direction = direction
Debug Lab 6: Szybka para kluczy wykonuje zawracanie obok czeku
proverka_ne_protiv_togo.py
def request_direction(self, direction):
    if is_reverse(self.state.next_direction, direction):   # kontra next_direction!
        return
    self.state.next_direction = direction
# Змейка едет вправо. Игрок быстро нажимает Up, потом Left —
# Left проходит проверку, потому что next_direction уже стало Up, а не Right.
Co widać na ekranie

Pułapka jest cienka: weryfikacja musi iść przeciwko state.direction jest kierunek, który TERAZ jest stosowany na kleszczu – a nie przeciwko temu, który już został zmieniony next_direction, w przeciwnym razie drugi klawisz między tykaczami może przeciągnąć nielegalny zwrot w stronę (U-turn).

Poprawiony kod
proverka_protiv_direction.py
def request_direction(self, direction):
    if is_reverse(self.state.direction, direction):
        return
    self.state.next_direction = direction
Debug Lab 7: Jedzenie nie jest wyrównane w sieci
neverno_vyrovnennaya_eda.py
def choose_food(snake, rng, *, half=280):
    x = rng.randint(-half, half)   # dowolna liczba całkowita, a nie wielokrotność STEP!
    y = rng.randint(-half, half)
    return (x, y)
# Яблоко появляется в точке вроде (137, -52) —
# голова змейки никогда не попадёт туда ровно, съесть его невозможно.
Co widać na ekranie

Rozdział 19.9 wyjaśniona: pozycje prawne to węzły siatki z nachyleniem STEP. choose_food() musi wybrać z tego samego zestawu komórek co next_head() , w przeciwnym razie matematycznie niemożliwe jest dokładne wejście do klatki z jedzeniem.

Poprawiony kod
vyrovnennaya_eda.py
def choose_food(snake, rng, *, half=280, step=20):
    free = tuple(c for c in all_cells(half=half, step=step) if c not in set(snake))
    return rng.choice(free)
Debug Lab 8: Jedzenie pojawia się wewnątrz ciała węża
eda_bez_proverki.py
def choose_food(snake, rng, *, half=280, step=20):
    coords = range(-half, half + 1, step)
    return (rng.choice(coords), rng.choice(coords))   # nie sprawdza, snake!
# При достаточно длинной змейке яблоко иногда появляется
# прямо внутри собственного тела — заведомо несъедобное.
Co widać na ekranie

Rozdział 19.18 omawiał dokładnie ten problem: bez wyjątku zajętych komórek choose_food() wybiera uczciwie losowo spośród WSZYSTKICH pól, w tym zajętych ciałem — co technicznie jest losowe, ale nieuczciwe wobec gracza.

Poprawiony kod
eda_s_proverkoj.py
def choose_food(snake, rng, *, half=280, step=20):
    occupied = set(snake)
    free = tuple(c for c in all_cells(half=half, step=step) if c not in occupied)
    return rng.choice(free)
Debug Lab 9: Ciało jest odnawiane od głowy po ogon
ot_golovy_k_hvostu.py
def dvigat_telo():
    for indeks in range(len(segmenty) - 1):   #wrong kierunek jazdy na rowerze!
        segmenty[indeks].goto(segmenty[indeks + 1].xcor(), segmenty[indeks + 1].ycor())
# Через несколько шагов все сегменты тела
# визуально схлопываются в одну точку.
Co widać na ekranie

Rozdział 19.17 szczegółowo omawiała ten temat: aktualizacja musi przejść od ogona do główka, w przeciwnym razie każdy kolejny segment odczytuje już nadpisane, a nie starą pozycję sąsiada.

Poprawiony kod
s_hvosta_k_golove.py
def dvigat_telo():
    for indeks in range(len(segmenty) - 1, 0, -1):
        segmenty[indeks].goto(segmenty[indeks - 1].xcor(), segmenty[indeks - 1].ycor())
Debug Lab 10: Tablica wyników bez clear()
tablo_bez_clear.py
def obnovit_tablo():
    tablo.write(f"Wynik: {{schet}}", align="center", font=("Arial", 16, "normal"))
    #tablo.clear() nie wywołane
# После нескольких съеденных яблок надпись на табло
# превращается в нечитаемую кашу из наложенных друг на друга цифр.
Co widać na ekranie

Rozdział 19.5 wyjaśniona: write() nie zastępuje poprzedniego tekstu, lecz rysuje na nim. clear() jest obowiązkowe przed każdym nowym wpisem tego samego żółwia.

Poprawiony kod
tablo_s_clear.py
def obnovit_tablo():
    tablo.clear()
    tablo.write(f"Wynik: {{schet}}", align="center", font=("Arial", 16, "normal"))
Debug Lab 11: Granica nie jest ściśle kontrolowana – głowa nie sięga krawędzi
granica_s_ravno.py
def is_wall_collision(head, *, half=280):
    x, y = head
    return abs(x) >= half or abs(y) >= half   #>=, nie >
# Игра заканчивается на одну клетку раньше настоящей границы —
# змейка никогда не может доехать до последнего легального ряда клеток.
Co widać na ekranie

Rozdział 19.19 wyjaśniona: GRANICA to współrzędna środka SEGMENTU, który nadal mieści się całkowicie na polu. >= błędnie wyklucza samą granicę z legalnego obszaru.

Poprawiony kod
granica_strogo.py
def is_wall_collision(head, *, half=280):
    x, y = head
    return abs(x) > half or abs(y) > half
Debug Lab 12: Samozderzenie sprawdzone dla STAREGO ciała
stolknovenie_po_staromu.py
grow = head == state.food
if is_self_collision(head, state.snake[1:]):   # PRZED move_snake()!
    state.status = GameStatus.GAME_OVER
# Змейка не растёт, а игрок пытается заехать в клетку, которую
# хвост как раз освобождает в этом же тике — игра ошибочно завершается.
Co widać na ekranie

Rozdział 19.20 dotyczyła właśnie tego: stara klatka ogonowa nadal jest w środku state.snake TO move_snake(). Musisz sprawdzić ciało PO przeprowadzce — new_snake[1:] — gdzie ogon już został uczciwie odcięty, jeśli wąż nie rośnie.

Poprawiony kod
stolknovenie_po_novomu.py
new_snake = move_snake(state.snake, head, grow=grow)
if is_self_collision(head, new_snake[1:]):
    state.status = GameStatus.GAME_OVER
Debug Lab 13:Restart zostawia stary łańcuch kleszczowy przy życiu
restart_bez_generation.py
def restart(self):
    self.state = new_game_state(self.rng)
    self.render()
    #generation nie powiększone – stary _on_timer() wciąż jest planowany!
# Через мгновение после Restart на экране внезапно появляется
# фигура/движение от уже несуществующей прошлой игры.
Co widać na ekranie

Rozdział 19.23 zajmowała się tą sprawą, klasycznym błędem „dwóch równoległych łańcuchów timera” _on_timer() nie różni się od nowego – oba działają jednocześnie.

Poprawiony kod
restart_s_generation.py
def restart(self):
    self._generation += 1
    self.state = new_game_state(self.rng, high_score=self.state.high_score)
    self.render()
Debug Lab 14: Zatrzymaj ekran, ale gra nadal się porusza
pauza_tolko_vizualno.py
def toggle_pause(self):
    self._show_overlay(„PAUZA”, „Space kontynuować”)
    # state.status niezmienione – game_tick() nic nie wie o pauzie!
# Оверлей «ПАУЗА» показан, но змейка под ним
# продолжает двигаться и может врезаться в стену.
Co widać na ekranie

Rozdział 19.22 wyjaśniona: nakładka to tylko coś, co jest WIDOCZNE. Prawdziwy stop wynika z czeku status is not RUNNING na samym początku game_tick() — bez zmiany state.status ten czek nigdy nie zadziała.

Poprawiony kod
pauza_po_statusu.py
def toggle_pause(self):
    if self.state.status is GameStatus.RUNNING:
        self.state.status = GameStatus.PAUSED
        self._show_overlay(„PAUZA”, „Space kontynuować”)
Debug Lab 15: Nowa gra nie resetuje kierunku
restart_bez_napravleniya.py
def restart(self):
    self._generation += 1
    self.state.snake = [(0, 0)]   # poprawia pole punktowo, a nie tworzy state od nowa
    self.state.score = 0
    # direction/next_direction pozostały z poprzedniej gry!
# Новая змейка стоит в центре, но при первом же движении
# уезжает в направлении, в котором закончилась ПРОШЛАЯ игра.
Co widać na ekranie

Spot edytuje do istniejącego state łatwo zapomnieć o jednym z pól. new_game_state() (rozdział 19.25) tworzy całą strukturę od nowa — zapomnieć o oddzielnym polu w nowym obiekcie jest niemożliwe, albo jest w konstruktorze, albo kod się nie uruchomi.

Poprawiony kod
restart_novym_state.py
def restart(self):
    self._generation += 1
    self.state = new_game_state(self.rng, high_score=self.state.high_score)
Debug Lab: Rekord zostaje zresetowany przez wynik
restart_teryaet_rekord.py
def restart(self):
    self._generation += 1
    self.state = new_game_state(self.rng)   # high_score nie przekazano — znowu 0!
# Игрок набрал 90 очков, проиграл, нажал R —
# табло снова показывает «Рекорд: 0», как будто игра только что установлена.
Co widać na ekranie

Rozdział 19.25 wyjaśniał: high_score musi być wyraźnie przekazany ze starego state w nową. new_game_state() bez argumentu high_score używa wartości domyślnej — zero.

Poprawiony kod
restart_s_rekordom.py
def restart(self):
    self._generation += 1
    self.state = new_game_state(self.rng, high_score=self.state.high_score)
Debug Lab 17: GameState przechowuje obiekt Turtle
gamestate_s_turtle.py
@dataclass
class GameState:
    snake: list[Position]
    head_turtle: turtle.Turtle   # obiekt Turtle wewnątrz modelu!
# Тесты из раздела 19.30 падают с ошибкой создания Turtle,
# хотя проверяют только математику координат — окно им не нужно.
Co widać na ekranie

Rozdział 19.27 wyraźnie ostrzegała: GameState jest domeną danych, a nie kontenerem na widgety. Gdy tylko turtle.Turtlestaje się niemożliwe stworzenie stanu bez prawdziwego okna — cała użyteczność czystej logiki (Rozdział 19.26) znika.

Poprawiony kod
gamestate_bez_turtle.py
@dataclass
class GameState:
    snake: list[Position]
    # obiekty Turtle żyć w SnakeApp, a nie w GameState
Debug Lab 18: Tick planuje siebie, nawet gdy gra już nie jest RUNNING
tik_planiruet_vsegda.py
def _on_timer(self, generation):
    if generation != self._generation:
        return
    self.game_tick()
    self._schedule_next_tick()   # brak sprawdzania statusu!
# После Game Over или паузы змейка выглядит остановленной,
# но цепочка ontimer() продолжает тикать вхолостую в фоне.
Co widać na ekranie

game_tick() sam w sobie jest bezpieczny — po prostu nic nie robi, gdy status != RUNNING. Ale jeśli następny tik jest ZDECYDOWANIE zaplanowany, łańcuch nigdy nie zatrzyma się sam, marnując timer nawet po zakończeniu gry.

Poprawiony kod
tik_planiruet_esli_running.py
def _on_timer(self, generation):
    if generation != self._generation:
        return
    self.game_tick()
    if self.state.status is GameStatus.RUNNING:
        self._schedule_next_tick()
Praktyka: znalezienie błędu „Snake”
Automatyczna kontrola — funkcja diagnose(): pozwala według objawu ustalić przyczynę, również nieznany objaw jest obsługiwany
Otwórz praktykę →