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.
screen = turtle.Screen()
screen.title(„Wąż”)
# tracer(0) nie wywołano
# Игра работает, но каждое движение головы и КАЖДОГО сегмента
# перерисовывается отдельно — заметное мерцание и заметная задержка.
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.
screen = turtle.Screen()
screen.title(„Wąż”)
screen.tracer(0)
def render(self):
self.head.goto(*self.state.snake[0])
# ...pozostałe renderowanie...
# self.screen.update() nie wywołany
# Модель меняется на каждом тике (можно проверить print()),
# но на экране змейка стоит неподвижно.
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.
def render(self):
self.head.goto(*self.state.snake[0])
# ...pozostałe renderowanie...
self.screen.update()
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
# Игру нельзя поставить на паузу: событие обрабатывается,
# но цикл ничего не проверяет и продолжает крутиться дальше.
# Скорость задана одной константой и по ходу партии не меняется.
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”
def game_tick(self):
if self.state.status is not GameStatus.RUNNING:
return
# ...
self.screen.ontimer(self.game_tick, self.state.delay_ms)
def game_tick(self):
# ...
time.sleep(self.state.delay_ms / 1000)
self.game_tick()
# Окно замирает НАВСЕГДА, а не на delay_ms: тик зовёт сам себя
# без условия выхода, поэтому пауз становится бесконечно много,
# а примерно через 1000 вызовов программа падает с RecursionError.
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.
def game_tick(self):
# ...
self.screen.ontimer(self.game_tick, self.state.delay_ms)
def request_direction(self, direction):
self.state.next_direction = direction # bez weryfikacji!
# Змейка едет вправо, игрок нажимает Left —
# голова немедленно врезается в первый сегмент собственного тела.
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.
def request_direction(self, direction):
if is_reverse(self.state.direction, direction):
return
self.state.next_direction = direction
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.
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).
def request_direction(self, direction):
if is_reverse(self.state.direction, direction):
return
self.state.next_direction = direction
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) —
# голова змейки никогда не попадёт туда ровно, съесть его невозможно.
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.
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)
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!
# При достаточно длинной змейке яблоко иногда появляется
# прямо внутри собственного тела — заведомо несъедобное.
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.
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)
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())
# Через несколько шагов все сегменты тела
# визуально схлопываются в одну точку.
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.
def dvigat_telo():
for indeks in range(len(segmenty) - 1, 0, -1):
segmenty[indeks].goto(segmenty[indeks - 1].xcor(), segmenty[indeks - 1].ycor())
def obnovit_tablo():
tablo.write(f"Wynik: {{schet}}", align="center", font=("Arial", 16, "normal"))
#tablo.clear() nie wywołane
# После нескольких съеденных яблок надпись на табло
# превращается в нечитаемую кашу из наложенных друг на друга цифр.
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.
def obnovit_tablo():
tablo.clear()
tablo.write(f"Wynik: {{schet}}", align="center", font=("Arial", 16, "normal"))
def is_wall_collision(head, *, half=280):
x, y = head
return abs(x) >= half or abs(y) >= half #>=, nie >
# Игра заканчивается на одну клетку раньше настоящей границы —
# змейка никогда не может доехать до последнего легального ряда клеток.
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.
def is_wall_collision(head, *, half=280):
x, y = head
return abs(x) > half or abs(y) > half
grow = head == state.food
if is_self_collision(head, state.snake[1:]): # PRZED move_snake()!
state.status = GameStatus.GAME_OVER
# Змейка не растёт, а игрок пытается заехать в клетку, которую
# хвост как раз освобождает в этом же тике — игра ошибочно завершается.
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.
new_snake = move_snake(state.snake, head, grow=grow)
if is_self_collision(head, new_snake[1:]):
state.status = GameStatus.GAME_OVER
def restart(self):
self.state = new_game_state(self.rng)
self.render()
#generation nie powiększone – stary _on_timer() wciąż jest planowany!
# Через мгновение после Restart на экране внезапно появляется
# фигура/движение от уже несуществующей прошлой игры.
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.
def restart(self):
self._generation += 1
self.state = new_game_state(self.rng, high_score=self.state.high_score)
self.render()
def toggle_pause(self):
self._show_overlay(„PAUZA”, „Space kontynuować”)
# state.status niezmienione – game_tick() nic nie wie o pauzie!
# Оверлей «ПАУЗА» показан, но змейка под ним
# продолжает двигаться и может врезаться в стену.
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.
def toggle_pause(self):
if self.state.status is GameStatus.RUNNING:
self.state.status = GameStatus.PAUSED
self._show_overlay(„PAUZA”, „Space kontynuować”)
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!
# Новая змейка стоит в центре, но при первом же движении
# уезжает в направлении, в котором закончилась ПРОШЛАЯ игра.
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.
def restart(self):
self._generation += 1
self.state = new_game_state(self.rng, high_score=self.state.high_score)
def restart(self):
self._generation += 1
self.state = new_game_state(self.rng) # high_score nie przekazano — znowu 0!
# Игрок набрал 90 очков, проиграл, нажал R —
# табло снова показывает «Рекорд: 0», как будто игра только что установлена.
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.
def restart(self):
self._generation += 1
self.state = new_game_state(self.rng, high_score=self.state.high_score)
@dataclass
class GameState:
snake: list[Position]
head_turtle: turtle.Turtle # obiekt Turtle wewnątrz modelu!
# Тесты из раздела 19.30 падают с ошибкой создания Turtle,
# хотя проверяют только математику координат — окно им не нужно.
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.
@dataclass
class GameState:
snake: list[Position]
# obiekty Turtle żyć w SnakeApp, a nie w GameState
def _on_timer(self, generation):
if generation != self._generation:
return
self.game_tick()
self._schedule_next_tick() # brak sprawdzania statusu!
# После Game Over или паузы змейка выглядит остановленной,
# но цепочка ontimer() продолжает тикать вхолостую в фоне.
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.
def _on_timer(self, generation):
if generation != self._generation:
return
self.game_tick()
if self.state.status is GameStatus.RUNNING:
self._schedule_next_tick()