9.6. Gra w życie
Gra w życie jest najbardziej znaną implementacją automatu komórkowego, wymyśloną przez brytyjskiego matematyka Johna Conwaya. Cały pomysł polega na symulowaniu rozwoju populacji komórek, które umieszczone w wyznaczonym obszarze tworzą różne zaskakujące układy.
Grę zaimplementujemy przy użyciu programowania obiektowego, którego podstawowym elementem są klasy. Można je rozumieć jako definicje obiektów odwzorowujących mniej lub bardziej dokładniej jakieś elementy rzeczywistości, niekoniecznie materialne. Obiekty łączą dane, czy też właściwości, oraz metody na nich operujące. Obiekt tworzymy na podstawie klas i nazywamy je wtedy instancjami danej klasy.
9.6.1. Plansza gry
Zaczniemy od przygotowania obszaru, w którym będziemy obserwować kolejne populacje
komórek. Tworzymy pusty plik w katalogu mcpi-sim
i zapisujemy pod nazwą mcpi-glife.py
.
Wstawiamy do niego poniższy kod:
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# import sys
5import os
6from random import randint
7from time import sleep
8import mcpi.minecraft as minecraft # import modułu minecraft
9import mcpi.block as block # import modułu block
10
11os.environ["USERNAME"] = "Steve" # nazwa użytkownika
12os.environ["COMPUTERNAME"] = "mykomp" # nazwa komputera
13
14mc = minecraft.Minecraft.create("192.168.1.10") # połączenie z MCPi
15
16
17class GraWZycie(object):
18 """
19 Łączy wszystkie elementy gry w całość.
20 """
21
22 def __init__(self, mc, szer, wys, ile=40):
23 """
24 Przygotowanie ustawień gry
25 :param szer: szerokość planszy mierzona liczbą komórek
26 :param wys: wysokość planszy mierzona liczbą komórek
27 """
28 self.mc = mc
29 mc.postToChat('Gra o zycie')
30 self.szer = szer
31 self.wys = wys
32
33 def uruchom(self):
34 """
35 Główna pętla gry
36 """
37 self.plac(0, 0, 0, self.szer, self.wys) # narysuj pole gry
38
39 def plac(self, x, y, z, szer=20, wys=10):
40 """
41 Funkcja tworzy plac gry
42 """
43 podloga = block.STONE
44 wypelniacz = block.AIR
45 granica = block.OBSIDIAN
46
47 # granica, podłoże, czyszczenie
48 self.mc.setBlocks(
49 x - 5, y, z - 5,
50 x + szer + 5, y + max(szer, wys), z + wys + 5, wypelniacz)
51 self.mc.setBlocks(
52 x - 1, y - 1, z - 1, x + szer + 1, y - 1, z + wys + 1, granica)
53 self.mc.setBlocks(x, y - 1, z, x + szer, y - 1, z + wys, podloga)
54 self.mc.setBlocks(
55 x, y, z, x + szer, y + max(szer, wys), z + wys, wypelniacz)
56
57
58if __name__ == "__main__":
59 gra = GraWZycie(mc, 20, 10, 40) # instancja klasy GraWZycie
60 mc.player.setPos(10, 20, -5)
61 gra.uruchom() # wywołanie metody uruchom()
Główna klasa w programie nazywa się GraWZycie
, jej definicja rozpoczyna się słowem
kluczowym class
, a nazwa obowiązkową dużą literą. Pierwsza zdefiniowana metoda o nazwie __init__()
to konstruktor klasy, wywoływany w momencie tworzenia jej instancji.
Dzieje się tak w głównej funkcji main()
w instrukcji: gra = GraWZycie(mc, 20, 10, 40)
.
Tworząc instancję klasy, czyli obiekt gra
, przekazujemy do konstruktora
parametry: obiekt mc
reprezentujący grę Minecraft, szerokość
i wysokość pola gry, a także ilość tworzonych na wstępie komórek.
Konstruktor z przekazanych parametrów tworzy właściwości klasy w instrukcjach
typu self.mc = mc
. Do właściwości klasy odwołujemy się w innych metodach za pomocą
słowa self
– np. w wywołanej w funkcji głównej metodzie uruchom()
.
Jej zadaniem jest wykonanie metody plac()
, która buduje planszę gry.
Przekazujemy jej współrzędne punktu początkowego, a także szerokość i wysokość planszy.
Informacja
Warto zauważyć i zapamiętać, że każda metoda w klasie jako pierwszy
parametr przyjmuje zawsze wskaźnik do instancji obiektu, na którym
będzie działać, czyli konwencjonalne słowo self
.
W wyniku uruchomienia i przetestowania kodu powinniśmy zobaczyć zbudowaną planszę do gry, czyli prostokąt, o podanych w funkcji głównej wymiarach.

9.6.2. Populacja
Utworzymy klasę Populacja
, a w niej strukturę danych reprezentującą
układ żywych i martwych komórek. Przed funkcją główną main()
wstawiamy kod:
61# magiczne liczby używane do określenia czy komórka jest żywa
62DEAD = 0
63ALIVE = 1
64BLOK_ALIVE = 35 # block.WOOL
65
66
67class Populacja(object):
68 """
69 Populacja komórek
70 """
71
72 def __init__(self, mc, ilex, iley):
73 """
74 Przygotowuje ustawienia populacji
75 :param mc: obiekt Minecrafta
76 :param ilex: rozmiar x macierzy komórek (wiersze)
77 :param iley: rozmiar y macierzy komórek (kolumny)
78 """
79 self.mc = mc
80 self.iley = iley
81 self.ilex = ilex
82 self.generacja = self.reset_generacja()
83
84 def reset_generacja(self):
85 """
86 Tworzy i zwraca macierz pustej populacji
87 """
88 # wyrażenie listowe tworzy x kolumn o y komórkach
89 # wypełnionych wartością 0 (DEAD)
90 return [[DEAD for y in xrange(self.iley)] for x in xrange(self.ilex)]
91
92 def losuj(self, ile=50):
93 """
94 Losowo wypełnia macierz żywymi komórkami, czyli wartością 1 (ALIVE)
95 """
96 for i in range(ile):
97 x = randint(0, self.ilex - 1)
98 y = randint(0, self.iley - 1)
99 self.generacja[x][y] = ALIVE
100 print self.generacja
Konstruktor klasy Populacja
pobiera obiekt Minecrafta (mc
) oraz rozmiary
dwuwymiarowej macierzy (ilex, iley
), czyli tablicy, która reprezentować będzie układy
komórek. Po przypisaniu właściwościom klasy przekazanych parametrów tworzymy
początkowy stan populacji, tj. macierz wypełnioną zerami. W metodzie reset_generacja()
wykorzystujemy wyrażenie listowe, które – ujmując rzecz w terminologii Pythona –
zwraca listę ilex list zawierających iley komórek z wartościami zero.
To właśnie wspomniana wcześniej macierz dwuwymiarowa.
Ćwiczenie 1
Uruchom konsolę IPython Qt Console i wklej do niej polecenia:
DEAD, ilex, iley = 0, 5, 10
generacja = [[DEAD for y in xrange(10)] for ilex in xrange(5)]
generacja
Zobacz efekt (nie zamykaj konsoli, jeszcze się przyda):

Komórki mogą być martwe (DEAD
– wartość 0) i tak jest na początku, ale aby populacja mogła ewoluować,
trzeba niektóre z nich ożywić (ALIVE
– wartość 1).
Odpowiada za to metoda losuj()
, która przyjmuje jeden argument
określający, ile komórek ma być początkowo żywych. Następnie w pętli losowana
jest wymagana ilość par indeksów wskazujących wiersz i kolumnę, czyli komórkę,
która ma być żywa (ALIVE
). Na końcu drukujemy w terminalu
początkowy układ komórek.
Ćwiczenie 2
Spróbuj w kilku komórkach macierzy utworzonej w konsoli, zapisać wartość ALIVE, czyli 1.
W konstruktorze klasy głównej GraWZycie
tworzymy instancję klasy Populacja
– to powoduje
wykonanie jej konstruktora. Potem wywołujemy metodę tworzącą układ początkowy.
Tak więc na końcu konstruktora klasy GraWZycie
(__init__()
)dodajemy poniższy kod:
32 self.populacja = Populacja(mc, szer, wys) # instancja klasy Populacja
33 if ile:
34 self.populacja.losuj(ile)
Przetestuj kod.
9.6.3. Rysowanie macierzy
Skoro mamy przygotowany plac gry oraz początkowy układ populacji, trzeba ją
narysować, czyli umieścić określone bloki we współrzędnych Minecrafta odpowiadających
indeksom ożywionych komórek macierzy. Na końcu klasy Populacja
dodajemy dwie nowe
metody rysyj()
i zywe_komorki()
:
103 def rysuj(self):
104 """
105 Rysuje komórki na planszy, czyli umieszcza odpowiednie bloki
106 """
107 print "Rysowanie macierzy..."
108 for x, z in self.zywe_komorki():
109 podtyp = randint(0, 15)
110 mc.setBlock(x, 0, z, BLOK_ALIVE, podtyp)
111
112 def zywe_komorki(self):
113 """
114 Generator zwracający współrzędne żywych komórek.
115 """
116 for x in range(len(self.generacja)):
117 kolumna = self.generacja[x]
118 for y in range(len(kolumna)):
119 if kolumna[y] == ALIVE:
120 yield x, y # zwracamy współrzędne, jeśli komórka jest żywa
– a rysowanie wywołujemy w metodzie uruchom()
klasy GraWZycie
, dopisując:
41 self.populacja.rysuj()
Wyjaśnienia wymaga funkcja rysuj()
. W pętli pobieramy współrzędne
żywych komórek, które rozpakowywane są z 2-elementowej listy do zmiennych:
for x, z in self.zywe_komorki():
. Dalej losujemy podtyp bloku bawełny
i umieszczamy go we wskazanym miejscu.
Funkcja zywe_komorki()
to tzw. generator, co poznajemy po tym,
że zwraca wartości za pomocą słowa kluczowego yield
. Jej
działanie polega na przeglądaniu macierzy za pomocą zagnieżdżonych pętli
i zwracaniu współrzędnych „żywych”komórek.
Ćwiczenie 3
Odwołując się do utworzonej wcześniej przykładowej macierzy, przetestuj w konsoli poniższy kod:
for x in range(len(generacja)):
kolumna = generacja[x]
for y in range(len(kolumna)):
print x, y, " = ", generacja[x][y]
Różnica pomiędzy generatorem a zwykłą funkcją polega na tym, że zwykła funkcja po przeglądnięciu całej macierzy zwróciłaby od razu kompletną listę żywych komórek, a generator robi to „na żądanie”. Po napotkaniu żywej komórki zwraca jej współrzędne, zapamiętuje stan lokalnych pętli i czeka na następne wywołanie. Dzięki temu oszczędzamy pamięć, a dla dużych struktur także zwiększamy wydajność.
Uruchom kod, oprócz pola gry, powinieneś zobaczyć bloki reprezentujące pierwszą generację komórek.

9.6.4. Ewolucja – zasady gry
Jak można było zauważyć, rozgrywka toczy się na placu podzielonym na kwadratowe komórki, którego reprezentacją algorytmiczną jest macierz. Każda komórka ma maksymalnie ośmiu sąsiadów. To czy komórka przetrwa, zależy od ich ilości. Reguły są następujące:
Martwa komórka, która ma dokładnie 3 sąsiadów, staje się żywa w następnej generacji.
Żywa komórka z 2 lub 3 sąsiadami zachowuje swój stan, w innym przypadku umiera z powodu „samotności” lub „zatłoczenia”.
Kolejne generacje obliczamy w umownych jednostkach czasu.
Do kodu klasy Populacja
dodajemy dwie metody zawierające logikę gry:
128 def sasiedzi(self, x, y):
129 """
130 Generator zwracający wszystkich okolicznych sąsiadów
131 """
132 for nx in range(x - 1, x + 2):
133 for ny in range(y - 1, y + 2):
134 if nx == x and ny == y:
135 continue # pomiń współrzędne centrum
136 if nx >= self.ilex:
137 # sąsiad poza końcem planszy, bierzemy pierwszego w danym
138 # rzędzie
139 nx = 0
140 elif nx < 0:
141 # sąsiad przed początkiem planszy, bierzemy ostatniego w
142 # danym rzędzie
143 nx = self.ilex - 1
144 if ny >= self.iley:
145 # sąsiad poza końcem planszy, bierzemy pierwszego w danej
146 # kolumnie
147 ny = 0
148 elif ny < 0:
149 # sąsiad przed początkiem planszy, bierzemy ostatniego w
150 # danej kolumnie
151 ny = self.iley - 1
152
153 # zwróć stan komórki w podanych współrzędnych
154 yield self.generacja[nx][ny]
155
156 def nast_generacja(self):
157 """
158 Generuje następną generację populacji komórek
159 """
160 print "Obliczanie generacji..."
161 nast_gen = self.reset_generacja()
162 for x in range(len(self.generacja)):
163 kolumna = self.generacja[x]
164 for y in range(len(kolumna)):
165 # pobieramy wartości sąsiadów
166 # dla żywej komórki dostaniemy wartość 1 (ALIVE)
167 # dla martwej otrzymamy wartość 0 (DEAD)
168 # zwykła suma pozwala nam określić liczbę żywych sąsiadów
169 iluS = sum(self.sasiedzi(x, y))
170 if iluS == 3:
171 # rozmnażamy się
172 nast_gen[x][y] = ALIVE
173 elif iluS == 2:
174 # przechodzi do kolejnej generacji bez zmian
175 nast_gen[x][y] = kolumna[y]
176 else:
177 # za dużo lub za mało sąsiadów by przeżyć
178 nast_gen[x][y] = DEAD
179
180 # nowa generacja staje się aktualną generacją
181 self.generacja = nast_gen
Metoda nast_generacja()
wylicza kolejny stan populacji. Na początku
tworzymy pustą macierz naste_gen
wypełnioną zerami – tak jak w konstruktorze
klasy. Następnie przy użyciu dwóch zagnieżdżonych pętli for
–
takich samych jak w generatorze zywe_komorki()
– przeglądamy
wiersze, wydobywając z nich kolejne komórki i badamy ich otoczenie.
Najważniejszy krok algorytmu to określenie ilości żywych sąsiednich komórek,
co ma miejsce w instrukcji: iluS = sum(self.sasiedzi(x, y))
.
Funkcja sum()
sumuje zapisane w sąsiednich komórkach wartości,
zwracane przez generator sasiedzi()
. Generator ten wykorzystuje
zagnieżdżone pętle for
, aby uzyskać współrzędne sąsiednich komórek,
następnie w instrukcjach warunkowych if
sprawdza,
czy nie wychodzą one poza planszę.
Uwaga
„Gra w życie” zakłada, że symulacja toczy się na nieograniczonej planszy, jednak dla celów wizualizacji w MC Pi musimy przyjąć jakieś jej wymiary, a także podjąć decyzję, co ma się dziać, kiedy je przekraczamy. W naszej implementacji, kiedy badając stan sąsiada przekraczamy planszę, bierzemy pod uwagę stan komórki z przeciwległego końca wiersza lub kolumny.
Ćwiczenie 4
Na przykładzie utworzonej wcześniej macierzy przetestuj w konsoli kod:
x, y = 2, 2
for nx in range(x - 1, x + 2):
for ny in range(y - 1, y + 2):
print nx, ny, "=", generacja[nx][ny]
Jak widzisz, zwraca on wartości zapisane w komórkach otaczających wyznaczoną współrzędnymi x, y.
Wróćmy do metody nast_generacja()
. Po wywołaniu iluS = sum(self.sasiedzi(x, y))
,
wiemy już, ilu mamy wokół siebie sąsiadów. Dalej za pomocą instrukcji warunkowych,
np. if iluS == 3:
, sprawdzamy więc ich ilość i – zgodnie z regułami –
ożywiamy badaną komórkę, zachowujemy jej stan lub ją uśmiercamy.
Uzyskany stan zapisujemy w nowej macierzy nast_gen
. Po zbadaniu
wszystkich komórek nowa macierz reprezentująca nową generację nadpisuje
poprzednią: self.generacja = nast_gen
. Pozostaje ją narysować.
Zmieniamy metodę uruchom()
klasy GraWZycie
:
36 def uruchom(self):
37 """
38 Główna pętla gry
39 """
40 i = 0
41 while True: # działaj w pętli do momentu otrzymania sygnału do wyjścia
42 print("Generacja: " + str(i))
43 self.plac(0, 0, 0, self.szer, self.wys) # narysuj pole gry
44 self.populacja.rysuj()
45 self.populacja.nast_generacja()
46 i += 1
47 sleep(1)
Proces generowania i rysowania kolejnych generacji komórek dokonuje się
w zmienionej metodzie uruchom()
głównej klasy naszego skryptu.
Wykorzystujemy nieskończoną pętlę while True:
, w której:
rysujemy plac gry,
rysujemy aktualną populację,
wyliczamy następną generację,
wstrzymujemy działanie na sekundę
i wszystko powtarzamy.
Tak uruchomiony program możemy przerwać tylko „ręcznie” przerywając działanie skryptu.
Wskazówka
Uwaga: metoda zakończenia działania skryptu zależy od sposobu jego
uruchomienia i systemu operacyjnego. Np. w Linuksie skrypt uruchomiony
w terminalu poleceniem python skrypt.py
przerwiemy naciskając
CTRL+C lub bardziej radykalnie ALT+F4 (zamknięcie okna z terminalem).
Przetestuj skrypt!

9.6.5. Początek zabawy
Śledzenie ewolucji losowo przygotowanego układu komórek nie jest zazwyczaj
zbyt widowiskowe, zwłaszcza kiedy symulację przeprowadzamy na dużej planszy.
O wiele ciekawsza jest możliwość śledzenia zmian samodzielnie zaprojektowanego
układu początkowego. Dodajmy więc możliwość wczytywania takiego układu
bezpośrednio z Minecrafta. Do klasy Populacja
poniżej metody losuj()
dodajemy kod:
111 def wczytaj(self):
112 """
113 Funkcja wczytuje populację komórek z MC RPi
114 """
115 ileKom = 0
116 print "Proszę czekać, aktuzalizacja macierzy..."
117 for x in range(self.ilex):
118 for z in range(self.iley):
119 blok = self.mc.getBlock(x, 0, z)
120 if blok != block.AIR:
121 self.generacja[x][z] = ALIVE
122 ileKom += 1
123 print self.generacja
124 print "Żywych:", str(ileKom)
125 sleep(3)
Działanie metody wczytaj()
jest proste: za pomocą zagnieżdżonych pętli
pobieramy typ bloku z każdego miejsca placu gry: blok = self.mc.getBlock(x, 0, z)
.
Jeżeli na placu znajduje się jakikolwiek blok inny niż powietrze,
oznaczamy odpowiednią komórkę początkowej generacji, wskazywaną przez współrzędną
bloku jako żywą: self.generacja[x][z] = ALIVE
. Przy okazji zliczamy ilość takich komórek.
Wywołanie funkcji trzeba dopisać do konstruktora klasy GraWZycie
w następujący sposób:
33 if ile:
34 self.populacja.losuj(ile)
35 else:
36 self.populacja.wczytaj()
Jak widać wykonanie metody wczytaj()
zależne jest od wartości parametru ile
.
Tak więc jeżeli chcesz przetestować nową możliwość, w wywołaniu konstruktora w funkcji
głównej ustaw ten parametr na 0 (zero), np: gra = GraWZycie(mc, 30, 20, 0)
.
Informacja
Uwaga: przy dużych rozmiarach pola gry odczytywanie wszystkich bloków zajmuje dużo czasu! Przed testowaniem wczytywania własnych układów warto uruchomić skrypt przynajmniej raz, aby zbudować w MC Pi plac gry.
Nie pozostaje nic innego, jak zacząć się bawić. Można np. urządzić zawody: czyja populacja komórek utrzyma się dłużej – oczywiście warto wykluczyć budowanie znanych i udokumentowanych układów stałych.

Ćwiczenie 5
Dodaj do skryptu mechanizm kończący symulacji, kiedy na planszy nie ma już żadnych żywych komórek.

Źródła:
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:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: