4.6. Życie Conwaya (obj)¶
Gra w życie zrealizowana z użyciem biblioteki PyGame.

4.6.1. Przygotowanie¶
Do rozpoczęcia pracy z przykładem pobieramy szczątkowy kod źródłowy:
~/python101$ git checkout -f life/z1
4.6.2. Okienko gry¶
Na wstępie w pliku ~/python101/games/life.py
otrzymujemy kod który przygotuje okienko naszej gry:
Informacja
Ten przykład zakłada wcześniejsze zrealizowanie przykładu: Pong (obj), opisy niektórych cech wspólnych zostały tutaj wyraźnie pominięte. W tym przykładzie wykorzystujemy np. podobne mechanizmy do tworzenia okna i zarządzania główną pętlą naszej gry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | # coding=utf-8
import pygame
import pygame.locals
class Board(object):
"""
Plansza do gry. Odpowiada za rysowanie okna gry.
"""
def __init__(self, width, height):
"""
Konstruktor planszy do gry. Przygotowuje okienko gry.
:param width: szerokość w pikselach
:param height: wysokość w pikselach
"""
self.surface = pygame.display.set_mode((width, height), 0, 32)
pygame.display.set_caption('Game of life')
def draw(self, *args):
"""
Rysuje okno gry
:param args: lista obiektów do narysowania
"""
background = (0, 0, 0)
self.surface.fill(background)
for drawable in args:
drawable.draw_on(self.surface)
# dopiero w tym miejscu następuje fatyczne rysowanie
# w oknie gry, wcześniej tylko ustalaliśmy co i jak ma zostać narysowane
pygame.display.update()
class GameOfLife(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height, cell_size=10):
"""
Przygotowanie ustawień gry
:param width: szerokość planszy mierzona liczbą komórek
:param height: wysokość planszy mierzona liczbą komórek
:param cell_size: bok komórki w pikselach
"""
pygame.init()
self.board = Board(width * cell_size, height * cell_size)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
def run(self):
"""
Główna pętla gry
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.board.draw()
self.fps_clock.tick(15)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
# Ta część powinna być zawsze na końcu modułu (ten plik jest modułem)
# chcemy uruchomić naszą grę dopiero po tym jak wszystkie klasy zostaną zadeklarowane
if __name__ == "__main__":
game = GameOfLife(80, 40)
game.run()
|
W powyższym kodzie mamy podstawy potrzebne do uruchomienia gry:
~/python101$ python games/life.py
4.6.3. Tworzymy matrycę życia¶
Nasza gra polega na ułożenia komórek na planszy i obserwacji jak w kolejnych generacjach życie się zmienia, które komórki giną, gdzie się rozmnażają i wywołują efektowną wędrówkę oraz tworzenie się ciekawych struktur.
Zacznijmy od zadeklarowania zmiennych które zastąpią nam tzw. magiczne liczby.
W kodzie zamiast wartości 1
dla określenia żywej komórki i wartości 0
dla martwej komórki wykorzystamy zmiennie ALIVE
oraz DEAD
. W innych językach
takie zmienne czasem są określane jako stała.
77 78 79 | # magiczne liczby używane do określenia czy komórka jest żywa
DEAD = 0
ALIVE = 1
|
Podstawą naszego życia będzie klasa Population
która będzie przechowywać stan gry,
a także realizować funkcje potrzebne do zmian stanu gry w czasie. W przeciwieństwie do
gry w Pong nie będziemy dzielić odpowiedzialności
pomiędzy większą liczbę klas.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | class Population(object):
"""
Populacja komórek
"""
def __init__(self, width, height, cell_size=10):
"""
Przygotowuje ustawienia populacji
:param width: szerokość planszy mierzona liczbą komórek
:param height: wysokość planszy mierzona liczbą komórek
:param cell_size: bok komórki w pikselach
"""
self.box_size = cell_size
self.height = height
self.width = width
self.generation = self.reset_generation()
def reset_generation(self):
"""
Tworzy i zwraca macierz pustej populacji
"""
# w pętli wypełnij listę kolumnami
# które także w pętli zostają wypełnione wartością 0 (DEAD)
return [[DEAD for y in range(self.height)] for x in range(self.width)]
|
Poza ostatnią linią nie ma tutaj wielu niespodzianek, ot konstruktor __init__
zapamiętujący wartości konfiguracyjne w instancji naszej klasy, tj. w self
.
W ostatniej linii budujemy macierz dla komórek. Tablicę dwuwymiarową, którą będziemy
adresować przy pomocy współrzędnych x
i y
.
Jeśli plansza miałaby szerokość 4, a wysokość 3
komórek to zadeklarowana ręcznie nasza tablica wyglądałaby tak:
1 2 3 4 5 | generation = [
[DEAD, DEAD, DEAD, DEAD],
[DEAD, DEAD, DEAD, DEAD],
[DEAD, DEAD, DEAD, DEAD],
]
|
Jednak ręczne zadeklarowanie byłoby uciążliwe i mało elastyczne, wyobraźmy sobie macierz 40 na 80 — strasznie dużo pisania! Dlatego posłużymy się pętlami i wyliczymy sobie dowolną macierz na podstawie zadanych parametrów.
1 2 3 4 5 6 7 8 | def reset_generation(self)
generation = []
for x in range(self.width):
column = []
for y in range(self.height)
column.append(DEAD)
generation.append(column)
return generation
|
Powyżej wykorzystaliśmy 2 pętle (jedna zagnieżdżona w drugiej)
oraz funkcję range
która wygeneruje listę wartości
od 0 do zadanej wartości - 1. Dzięki temu nasze pętle uzyskają self.width
i self.height
przebiegów. Jest lepiej.
Przykład kodu powyżej to konstrukcja którą w taki lub podobny sposób wykorzystuje się co chwila w każdym programie — to chleb powszedni programisty. Każdy program musi w jakiś sposób iterować po elementach list przekształcając je w inne listy.
W linii 113 mamy przykład zastosowania tzw. wyrażeń listowych (ang. list comprehensions).
Pomiędzy znakami nawiasów kwadratowych [ ]
mamy pętlę, która w każdym przebiegu
zwraca jakiś element. Te zwrócone elementy napełniają nową listę która zostanie zwrócona
w wyniku wyrażenia.
Sprawę komplikuje dodaje fakt, że chcemy uzyskać tablicę dwuwymiarową dlatego mamy zagnieżdżone wyrażenie listowe (jak 2 pętle powyżej). Zajrzyjmy najpierw do wewnętrznego wyrażenia:
1 | [DEAD for y in range(self.height)]
|
W kodzie powyżej każdym przebiegu pętli uzyskamy DEAD
. Dzięki temu zyskamy
kolumnę macierzy od wysokości self.height
, w każdej z nich będziemy mogli się dostać do pojedynczej
komorki adresując ją listę wartością y
o tak kolumna[y]
.
Teraz zajmijmy się zewnętrznym wyrażeniem
listowym, ale dla uproszczenia w każdym jego przebiegu zwracajmy nowa_kolumna
1 | [nowa_kolumna for x in range(self.width)]
|
W kodzie powyżej w każdym przebiegu pętli uzyskamy nowa_kolumna
. Dzięki temu zyskamy
listę kolumn. Do każdej z nich będziemy mogli się dostać adresując listę wartością x
o tak
generation[x]
, w wyniku otrzymamy kolumnę którą możemy adresować wartością y
, co w sumie
da nam macierz w której do komórek dostaniemy się o tak: generation[x][y]
.
Zamieniamy nowa_kolumna
wyrażeniem listowym dla y
i otrzymamy 1 linijkę
zamiast 7 z przykładu z podwójną pętlą:
1 | [[DEAD for y in range(self.height)] for x in range(self.width)]
|
4.6.4. Układamy żywe komórki na planszy¶
Teraz przygotujemy kod który dzięki wykorzystaniu myszki umożliwi nam
ułożenie planszy, będziemy wybierać gdzie na planszy będą żywe komórki.
Dodajmy do klasy Population
metodę handle_mouse
którą będziemy
później wywoływać w metody GameOfLife.handle_events
za każdym razem
gdy nasz program otrzyma zdarzenie dotyczące myszki.
Chcemy by myszka z naciśniętym lewym klawiszem ustawiała pod kursorem żywą komórkę. Jeśli jest naciśnięty inny klawisz to usuniemy żywą komórkę. Jeśli żaden z klawiszy nie jest naciśnięty to zignorujemy zdarzenie myszki.
Zdarzenia są generowane w przypadku naciśnięcia klawiszy lub ruchu myszką, nie będziemy nic robić jeśli gracz poruszy myszką bez naciskania klawiszy.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | def handle_mouse(self):
# pobierz stan guzików myszki z wykorzystaniem funcji pygame
buttons = pygame.mouse.get_pressed()
if not any(buttons):
# ignoruj zdarzenie jeśli żaden z guzików nie jest wciśnięty
return
# dodaj żywą komórką jeśli wciśnięty jest pierwszy guzik myszki
# będziemy mogli nie tylko dodawać żywe komórki ale także je usuwać
alive = True if buttons[0] else False
# pobierz pozycję kursora na planszy mierzoną w pikselach
x, y = pygame.mouse.get_pos()
# przeliczamy współrzędne komórki z pikseli na współrzędne komórki w macierz
# gracz może kliknąć w kwadracie o szerokości box_size by wybrać komórkę
x /= self.box_size
y /= self.box_size
# ustaw stan komórki na macierzy
self.generation[int(x)][int(y)] = ALIVE if alive else DEAD
|
Następnie dodajmy metodę draw_on
która będzie rysować żywe komórki na planszy.
Tą metodę wywołamy w metodzie GameOfLife.draw
.
130 131 132 133 134 135 136 137 138 139 | def draw_on(self, surface):
"""
Rysuje komórki na planszy
"""
for x, y in self.alive_cells():
size = (self.box_size, self.box_size)
position = (x * self.box_size, y * self.box_size)
color = (255, 255, 255)
thickness = 1
pygame.draw.rect(surface, color, pygame.locals.Rect(position, size), thickness)
|
Powyżej wykorzystaliśmy nie istniejącą metodę alive_cells
która jak wynika z
jej użycia powinna zwrócić kolekcję współrzędnych dla żywych komórek.
Po jednej parze x, y
dla każdej żywej komórki. Każdą żywą komórkę
narysujemy jako kwadrat w białym kolorze.
Utwórzmy metodę alive_cells
która w pętli przejdzie po całej macierzy populacji
i zwróci tylko współrzędne żywych komórek.
141 142 143 144 145 146 147 148 149 150 | def alive_cells(self):
"""
Generator zwracający współrzędne żywych komórek.
"""
for x in range(len(self.generation)):
column = self.generation[x]
for y in range(len(column)):
if column[y] == ALIVE:
# jeśli komórka jest żywa zwrócimy jej współrzędne
yield x, y
|
W kodzie powyżej mamy przykład dwóch pętli przy pomocy których sprawdzamy zawartość
stan życia komórek dla wszystkich możliwych współrzędnych x
i y
w macierzy.
Na uwagę zasługują dwie rzeczy. Nigdzie tutaj nie zadeklarowaliśmy listy żywych komórek
— którą chcemy zwrócić — oraz instrukcję yield.
Instrukcja yield
powoduje, że nasza funkcja zamiast zwykłych wartości zwróci
generator. W skrócie w każdym przebiegu wewnętrznej pętli
zostaną wygenerowane i zwrócone na zewnątrz wartości x, y
. Za każdym razem gdy
for x, y in self.alive_cells()
poprosi o współrzędne następnej żywej komórki,
alive_cells
wykona się do instrukcji yield
.
Wskazówka
Działanie generatora najlepiej zaobserwować w debugerze, będziemy mogli to zrobić za chwilę.
4.6.5. Dodajemy populację do kontrolera gry¶
Czas by rozwinąć nasz kontroler gry, klasę GameOfLife
o instancję klasy Population
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | class GameOfLife(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height, cell_size=10):
"""
Przygotowanie ustawień gry
:param width: szerokość planszy mierzona liczbą komórek
:param height: wysokość planszy mierzona liczbą komórek
:param cell_size: bok komórki w pikselach
"""
pygame.init()
self.board = Board(width * cell_size, height * cell_size)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.population = Population(width, height, cell_size)
def run(self):
"""
Główna pętla gry
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.board.draw(
self.population,
)
self.fps_clock.tick(15)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
from pygame.locals import MOUSEMOTION, MOUSEBUTTONDOWN
if event.type == MOUSEMOTION or event.type == MOUSEBUTTONDOWN:
self.population.handle_mouse()
|
Gotowy kod możemy wyciągnąć komendą:
~/python101$ git checkout -f life/z2
4.6.6. Szukamy żyjących sąsiadów¶
Podstawą do określenia tego czy w danym miejscu na planszy (w współrzędnych x i y macierzy) powstanie nowe życie, przetrwa lub zginie istniejące życie; jest określenie liczby żywych komórek w bezpośrednim sąsiedztwie. Przygotujmy do tego metodę:
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | def neighbours(self, x, y):
"""
Generator zwracający wszystkich okolicznych sąsiadów
"""
for nx in range(x-1, x+2):
for ny in range(y-1, y+2):
if nx == x and ny == y:
# pomiń współrzędne centrum
continue
if nx >= self.width:
# sąsiad poza końcem planszy, bierzemy pierwszego w danym rzędzie
nx = 0
elif nx < 0:
# sąsiad przed początkiem planszy, bierzemy ostatniego w danym rzędzie
nx = self.width - 1
if ny >= self.height:
# sąsiad poza końcem planszy, bierzemy pierwszego w danej kolumnie
ny = 0
elif ny < 0:
# sąsiad przed początkiem planszy, bierzemy ostatniego w danej kolumnie
ny = self.height - 1
# dla każdego nie pominiętego powyżej
# przejścia pętli zwróć komórkę w tych współrzędnych
yield self.generation[nx][ny]
|
Następnie przygotujmy funkcję która będzie tworzyć nową populację
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | def cycle_generation(self):
"""
Generuje następną generację populacji komórek
"""
next_gen = self.reset_generation()
for x in range(len(self.generation)):
column = self.generation[x]
for y in range(len(column)):
# pobieramy wartości sąsiadów
# dla żywej komórki dostaniemy wartość 1 (ALIVE)
# dla martwej otrzymamy wartość 0 (DEAD)
# zwykła suma pozwala nam określić liczbę żywych sąsiadów
count = sum(self.neighbours(x, y))
if count == 3:
# rozmnażamy się
next_gen[x][y] = ALIVE
elif count == 2:
# przechodzi do kolejnej generacji bez zmian
next_gen[x][y] = column[y]
else:
# za dużo lub za mało sąsiadów by przeżyć
next_gen[x][y] = DEAD
# nowa generacja staje się aktualną generacją
self.generation = next_gen
|
Jeszcze ostatnie modyfikacje kontrolera gry tak by komórki zaczęły żyć po wciśnięciu klawisza enter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | class GameOfLife(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height, cell_size=10):
"""
Przygotowanie ustawień gry
:param width: szerokość planszy mierzona liczbą komórek
:param height: wysokość planszy mierzona liczbą komórek
:param cell_size: bok komórki w pikselach
"""
pygame.init()
self.board = Board(width * cell_size, height * cell_size)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.population = Population(width, height, cell_size)
def run(self):
"""
Główna pętla gry
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.board.draw(
self.population,
)
if getattr(self, "started", None):
self.population.cycle_generation()
self.fps_clock.tick(15)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
from pygame.locals import MOUSEMOTION, MOUSEBUTTONDOWN
if event.type == MOUSEMOTION or event.type == MOUSEBUTTONDOWN:
self.population.handle_mouse()
from pygame.locals import KEYDOWN, K_RETURN
if event.type == KEYDOWN and event.key == K_RETURN:
self.started = True
|
Gotowy kod możemy wyciągnąć komendą:
~/python101$ git checkout -f life/z3
4.6.7. Zadania dodatkowe¶
- TODO
Materiały Python 101
udostępniane przez
Centrum Edukacji Obywatelskiej na licencji
Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.
Utworzony: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |