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 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 | #!/usr/bin/env python
# -*- coding: utf-8 -*-
# import sys
import os
from random import randint
from time import sleep
import mcpi.minecraft as minecraft # import modułu minecraft
import mcpi.block as block # import modułu block
os.environ["USERNAME"] = "Steve" # nazwa użytkownika
os.environ["COMPUTERNAME"] = "mykomp" # nazwa komputera
mc = minecraft.Minecraft.create("192.168.1.10") # połączenie z MCPi
class GraWZycie(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, mc, szer, wys, ile=40):
"""
Przygotowanie ustawień gry
:param szer: szerokość planszy mierzona liczbą komórek
:param wys: wysokość planszy mierzona liczbą komórek
"""
self.mc = mc
mc.postToChat('Gra o zycie')
self.szer = szer
self.wys = wys
def uruchom(self):
"""
Główna pętla gry
"""
self.plac(0, 0, 0, self.szer, self.wys) # narysuj pole gry
def plac(self, x, y, z, szer=20, wys=10):
"""
Funkcja tworzy plac gry
"""
podloga = block.STONE
wypelniacz = block.AIR
granica = block.OBSIDIAN
# granica, podłoże, czyszczenie
self.mc.setBlocks(
x - 5, y, z - 5,
x + szer + 5, y + max(szer, wys), z + wys + 5, wypelniacz)
self.mc.setBlocks(
x - 1, y - 1, z - 1, x + szer + 1, y - 1, z + wys + 1, granica)
self.mc.setBlocks(x, y - 1, z, x + szer, y - 1, z + wys, podloga)
self.mc.setBlocks(
x, y, z, x + szer, y + max(szer, wys), z + wys, wypelniacz)
if __name__ == "__main__":
gra = GraWZycie(mc, 20, 10, 40) # instancja klasy GraWZycie
mc.player.setPos(10, 20, -5)
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | # magiczne liczby używane do określenia czy komórka jest żywa
DEAD = 0
ALIVE = 1
BLOK_ALIVE = 35 # block.WOOL
class Populacja(object):
"""
Populacja komórek
"""
def __init__(self, mc, ilex, iley):
"""
Przygotowuje ustawienia populacji
:param mc: obiekt Minecrafta
:param ilex: rozmiar x macierzy komórek (wiersze)
:param iley: rozmiar y macierzy komórek (kolumny)
"""
self.mc = mc
self.iley = iley
self.ilex = ilex
self.generacja = self.reset_generacja()
def reset_generacja(self):
"""
Tworzy i zwraca macierz pustej populacji
"""
# wyrażenie listowe tworzy x kolumn o y komórkach
# wypełnionych wartością 0 (DEAD)
return [[DEAD for y in xrange(self.iley)] for x in xrange(self.ilex)]
def losuj(self, ile=50):
"""
Losowo wypełnia macierz żywymi komórkami, czyli wartością 1 (ALIVE)
"""
for i in range(ile):
x = randint(0, self.ilex - 1)
y = randint(0, self.iley - 1)
self.generacja[x][y] = ALIVE
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 33 34 | self.populacja = Populacja(mc, szer, wys) # instancja klasy Populacja
if ile:
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | def rysuj(self):
"""
Rysuje komórki na planszy, czyli umieszcza odpowiednie bloki
"""
print "Rysowanie macierzy..."
for x, z in self.zywe_komorki():
podtyp = randint(0, 15)
mc.setBlock(x, 0, z, BLOK_ALIVE, podtyp)
def zywe_komorki(self):
"""
Generator zwracający współrzędne żywych komórek.
"""
for x in range(len(self.generacja)):
kolumna = self.generacja[x]
for y in range(len(kolumna)):
if kolumna[y] == ALIVE:
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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | def sasiedzi(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:
continue # pomiń współrzędne centrum
if nx >= self.ilex:
# 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.ilex - 1
if ny >= self.iley:
# 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.iley - 1
# zwróć stan komórki w podanych współrzędnych
yield self.generacja[nx][ny]
def nast_generacja(self):
"""
Generuje następną generację populacji komórek
"""
print "Obliczanie generacji..."
nast_gen = self.reset_generacja()
for x in range(len(self.generacja)):
kolumna = self.generacja[x]
for y in range(len(kolumna)):
# 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
iluS = sum(self.sasiedzi(x, y))
if iluS == 3:
# rozmnażamy się
nast_gen[x][y] = ALIVE
elif iluS == 2:
# przechodzi do kolejnej generacji bez zmian
nast_gen[x][y] = kolumna[y]
else:
# za dużo lub za mało sąsiadów by przeżyć
nast_gen[x][y] = DEAD
# nowa generacja staje się aktualną generacją
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 37 38 39 40 41 42 43 44 45 46 47 | def uruchom(self):
"""
Główna pętla gry
"""
i = 0
while True: # działaj w pętli do momentu otrzymania sygnału do wyjścia
print("Generacja: " + str(i))
self.plac(0, 0, 0, self.szer, self.wys) # narysuj pole gry
self.populacja.rysuj()
self.populacja.nast_generacja()
i += 1
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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | def wczytaj(self):
"""
Funkcja wczytuje populację komórek z MC RPi
"""
ileKom = 0
print "Proszę czekać, aktuzalizacja macierzy..."
for x in range(self.ilex):
for z in range(self.iley):
blok = self.mc.getBlock(x, 0, z)
if blok != block.AIR:
self.generacja[x][z] = ALIVE
ileKom += 1
print self.generacja
print "Żywych:", str(ileKom)
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 34 35 36 | if ile:
self.populacja.losuj(ile)
else:
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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |