Rozdział 21 · Projekt: kosmiczny strzelec

Jak uczynić grę wygodną do testów automatycznych

Czyste funkcje, Game(rng=...) a uruchamianie bez okna to gra, którą można sprawdzić automatycznie.

Gry wielkości tego projektu nie da się sprawdzić tylko ręcznym uruchomieniem — zbyt dużo kombinacji stanów. Ostateczna wersja napisana jest tak, aby można ją było testować automatycznie, bez żadnego rzeczywistego okna.

Funkcje czyste: logika bez Pygame

Formuły trudności (rozdział 21.17), współrzędna pojawienia się wroga (rozdział 21.14) i liczenie punktów (rozdział 21.15) — zwykłe funkcje od liczb i list, bez odwoływania się do self lub ekran:

fragment_pure_functions.py
def ochki_za_unichtozhennyh(vragi):
    return sum(vrag.points for vrag in vragi)

# Czysta funkcja nie potrzebuje prawdziwego Enemy ze sprite’em i obrazkiem —
# każdy obiekt posiadający atrybut points: jest wystarczający
class ZaglushkaVraga:
    def __init__(self, points):
        self.points = points

assert ochki_za_unichtozhennyh([]) == 0
assert ochki_za_unichtozhennyh(
    [ZaglushkaVraga(points=100), ZaglushkaVraga(points=200)]
) == 300

To nie obejście reguł, ale bezpośredni skutek tego, że ochki_za_unichtozhennyh w ogóle nie sprawdza, jakiej klasy są przekazane obiekty — potrzebuje tylko atrybutu .points dla wszystkich. Taka funkcja może być testowana przez każdego obiekt z tym atrybutem, aż do prostego stubu, bez rozpoczynania Pygame i bez tworzenia prawdziwi wrogowie.

Sterowany przypadek: Game(rng=...)

Gra przyjmuje generator liczb losowych jako jawny parametr, a nie odwołuje się do globalnego random bezpośrednio:

fragment_rng_injection.py
class Game:
    def __init__(self, *, rng=None):
        self.rng = rng if rng is not None else random.Random()

# w prawdziwej grze to typowa nieprzewidywalna losowość:
game = Game()

# w testach i przy tworzeniu zrzutów ekranu – powtarzalne:
game = Game(rng=random.Random(42))
Ten sam seed, ten sam rezultat
random.Random(42) z ustalonym ziarnem (seed) daje tę samą sekwencję losowych liczb przy każdym starcie – dzięki temu pojawienie się wrogów, ich typ i pozycja stają się powtarzalne: wygodne zarówno w testach, jak i scenariuszach deterministycznych – na przykład dla zrzutów ekranu z konkretnego, wcześniej znanego stanu gry.

Uruchamianie bez okien: Pusty sterownik SDL- (headless)

Ten sam trik co we wszystkich testach rozdziału 20: zmienne środowiskowe przełączają Pygame na "pusty" (dummy) sterownik wideo i audio SDL – okno fizycznie nie pojawia się nigdzie indziej, lecz całe Logika (Surface, Rect, kolizje, dźwięk) działa naprawdę. Taki start bez Realne okno w terminologii angielskiej nazywa się headless – "bez głowy", czyli bez ekran i brak interfejsu użytkownika.

fragment_headless_env.py
import os

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")

import space_shooter as ss

game = ss.Game()   # prawdziwa Game, brak prawdziwego okna
Nie wymaga ciężkiej pracy przy prostym importie modułu
space_shooter.py nie powoduje pygame.init() lub Game().run() na poziomie modułu — cała inicjalizacja odbywa się wewnętrznie Game.__init__(), ale nieskończona pętla występuje tylko wewnątrz if __name__ == "__main__":. W przeciwnym razie to po prostu import space_shooter w teście prowadziłby prawdziwą grę.