4.5. Życie Conwaya (str)

Gra w życie zrealizowana z użyciem biblioteki PyGame. Wersja strukturalna. Biblioteka PyGame ułatwia tworzenie aplikacji multimedialnych, w tym gier.

../../_images/life.png

4.5.1. Zmienne i plansza gry

Tworzymy plik life.py w terminalu lub w wybranym edytorze i zaczynamy od zdefiniowania zmiennych określających właściwości obiektów w naszej grze.

Kod nr
 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
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import pygame
import sys
import random
from pygame.locals import *  # udostępnienie nazw metod z locals

# inicjacja modułu pygame
pygame.init()

# szerokość i wysokość okna gry
OKNOGRY_SZER = 800
OKNOGRY_WYS = 400

# przygotowanie powierzchni do rysowania, czyli inicjacja okna gry
OKNOGRY = pygame.display.set_mode((OKNOGRY_SZER, OKNOGRY_WYS), 0, 32)
# tytuł okna gry
pygame.display.set_caption('Gra o życie')

# rozmiar komórki
ROZ_KOM = 10
# ilość komórek w poziomie i pionie
KOM_POZIOM = int(OKNOGRY_SZER / ROZ_KOM)
KOM_PION = int(OKNOGRY_WYS / ROZ_KOM)

# wartości oznaczające komórki "martwe" i "żywe"
KOM_MARTWA = 0
KOM_ZYWA = 1

# lista opisująca stan pola gry, 0 - komórki martwe, 1 - komórki żywe
# na początku tworzymy listę zawierającą KOM_POZIOM zer
POLE_GRY = [KOM_MARTWA] * KOM_POZIOM
# rozszerzamy listę o listy zagnieżdżone, otrzymujemy więc listę dwuwymiarową
for i in range(KOM_POZIOM):
    POLE_GRY[i] = [KOM_MARTWA] * KOM_PION

W instrukcji pygame.display.set_mode() inicjalizujemy okno gry o rozmiarach 800x400 pikseli i 32-bitowej głębi kolorów. Tworzymy w ten sposób powierzchnię główną do rysowania zapisaną w zmiennej OKNOGRY. Ilość możliwych do narysowania komórek, reprezentowanych przez kwadraty o boku 10 pikseli, wyliczamy w zmiennych KOM_POZIOM i KOM_PION. Najważniejszą strukturą w naszej grze jest POLE_GRY, dwuwymiarowa lista elementów reprezentujących “żywe” i “martwe” komórki, czyli populację. Tworzymy ją w dwóch krokach, na początku inicjujemy zerami jednowymiarową listę o rozmiarze odpowiadającym ilości komórek w poziomie (POLE_GRY = [KOM_MARTWA] * KOM_POZIOM). Następnie do każdego elementu listy przypisujemy listę zawierającą tyle zer, ile jest komórek w pionie.

4.5.2. Populacja komórek

Kolejnym krokiem będzie zdefiniowanie funkcji przygotowującej i rysującej populację komórek.

Kod nr
 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
 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
107
108
109
110
111
112
113
114
115
116
117
118
119
# przygotowanie następnej generacji komórek, czyli zaktualizowanego POLA_GRY
def przygotuj_populacje(polegry):
    # na początku tworzymy 2-wymiarową listę wypełnioną zerami
    nast_gen = [KOM_MARTWA] * KOM_POZIOM
    for i in range(KOM_POZIOM):
        nast_gen[i] = [KOM_MARTWA] * KOM_PION

    # iterujemy po wszystkich komórkach
    for y in range(KOM_PION):
        for x in range(KOM_POZIOM):

            # zlicz populację (żywych komórek) wokół komórki
            populacja = 0
            # wiersz 1
            try:
                if polegry[x - 1][y - 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass
            try:
                if polegry[x][y - 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass
            try:
                if polegry[x + 1][y - 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass

            # wiersz 2
            try:
                if polegry[x - 1][y] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass
            try:
                if polegry[x + 1][y] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass

            # wiersz 3
            try:
                if polegry[x - 1][y + 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass
            try:
                if polegry[x][y + 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass
            try:
                if polegry[x + 1][y + 1] == KOM_ZYWA:
                    populacja += 1
            except IndexError:
                pass

            # "niedoludnienie" lub przeludnienie = śmierć komórki
            if polegry[x][y] == KOM_ZYWA and (populacja < 2 or populacja > 3):
                nast_gen[x][y] = KOM_MARTWA
            # życie trwa
            elif polegry[x][y] == KOM_ZYWA \
                    and (populacja == 3 or populacja == 2):
                nast_gen[x][y] = KOM_ZYWA
            # nowe życie
            elif polegry[x][y] == KOM_MARTWA and populacja == 3:
                nast_gen[x][y] = KOM_ZYWA

    # zwróć nowe polegry z następną generacją komórek
    return nast_gen


def rysuj_populacje():
    """Rysowanie komórek (kwadratów) żywych"""
    for y in range(KOM_PION):
        for x in range(KOM_POZIOM):
            if POLE_GRY[x][y] == KOM_ZYWA:
                pygame.draw.rect(OKNOGRY, (255, 255, 255), Rect(
                    (x * ROZ_KOM, y * ROZ_KOM), (ROZ_KOM, ROZ_KOM)), 1)

Najważniejszym fragmentem kodu, implementującym logikę naszej gry, jest funkcja przygotuj_populacje(), która jako parametr przyjmuje omówioną wcześniej strukturę POLE_GRY (pod nazwą polegry). Funkcja sprawdza, jak rozwija się populacja komórek, według następujących zasad:

  1. Jeżeli żywa komórka ma mniej niż 2 żywych sąsiadów, umiera z powodu samotności.
  2. Jeżeli żywa komórka ma więcej niż 3 żywych sąsiadów, umiera z powodu przeludnienia.
  3. Żywa komórka z 2 lub 3 sąsiadami żyje dalej.
  4. Martwa komórka z 3 żywymi sąsiadami ożywa.

Funkcja iteruje po każdym elemencie POLA_GRY i sprawdza stan sąsiadów każdej komórki, w wierszu 1 powyżej komórki, w wierszu 2 na tym samym poziomie i w wierszu 3 poniżej. Konstrukcja try...except pozwala obsłużyć sytuacje wyjątkowe (błędy), a więc komórki skrajne, które nie mają sąsiadów u góry czy u dołu, z lewej bądź z prawej strony: w takim przypadku wywoływana jest instrukcja pass, czyli nie rób nic :-). Końcowa złożona instrukcja warunkowa if ożywia lub uśmierca sprawdzaną komórkę w zależności od stanu sąsiednich komórek (czyli zmiennej populacja).

Zadaniem funkcji rysuj_populacje() jest narysowanie kwadratów (obiekty Rect) o białych bokach w rozmiarze 10 pikseli dla pól (elementów), które w liście POLE_GRY są żywe (mają wartość 1).

4.5.3. Główna pętla programu

Programy interaktywne, w tym gry, reagujące na działania użytkownika, takie jak ruchy czy kliknięcia myszą, działają w pętli, której zadaniem jest:

  1. przechwycenie i obsługa działań użytkownika, czyli tzw. zdarzeń (ruchy, kliknięcia myszą, naciśnięcie klawiszy),
  2. aktualizacja stanu gry (przesunięcia elementów, aktualizacja planszy),
  3. aktualizacja wyświetlanego okna (narysowanie nowego stanu gry).

Dopisujemy więc do kodu główną pętlę wraz z obsługą zdarzeń:

Kod nr
122
123
124
125
126
127
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
# zmienne sterujące wykorzystywane w pętli głównej
zycie_trwa = False
przycisk_wdol = False

# pętla główna programu
while True:
    # obsługa zdarzeń generowanych przez gracza
    for event in pygame.event.get():
        # przechwyć zamknięcie okna
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

        if event.type == KEYDOWN and event.key == K_RETURN:
            zycie_trwa = True

        if zycie_trwa is False:
            if event.type == MOUSEBUTTONDOWN:
                przycisk_wdol = True
                przycisk_typ = event.button

            if event.type == MOUSEBUTTONUP:
                przycisk_wdol = False

            if przycisk_wdol:
                mouse_x, mouse_y = pygame.mouse.get_pos()
                mouse_x = int(mouse_x / ROZ_KOM)
                mouse_y = int(mouse_y / ROZ_KOM)
                # lewy przycisk myszy ożywia
                if przycisk_typ == 1:
                    POLE_GRY[mouse_x][mouse_y] = KOM_ZYWA
                # prawy przycisk myszy uśmierca
                if przycisk_typ == 3:
                    POLE_GRY[mouse_x][mouse_y] = KOM_MARTWA

    if zycie_trwa is True:
        POLE_GRY = przygotuj_populacje(POLE_GRY)

    OKNOGRY.fill((0, 0, 0))  # ustaw kolor okna gry
    rysuj_populacje()
    pygame.display.update()
    pygame.time.delay(100)

W obrębie głównej pętli programu pętla for odczytuje kolejne zdarzenia zwracane przez metodę pygame.event.get(). Jak widać, w pierwszej kolejności obsługujemy wydarzenie typu (właściwość .type) QUIT, czyli zakończenie aplikacji.

Jednak na początku gry gracz klika lewym lub prawym klawiszem myszy i ożywia lub uśmierca kliknięte komórki w obrębie okna gry. Dzieje się tak dopóty, dopóki zmienna zycie_trwa ma wartość False, a więc dopóki gracz nie naciśnie klawisza ENTER (if event.type == KEYDOWN and event.key == K_RETURN:). Każde kliknięcie myszą zostaje przechwycone (if event.type == MOUSEBUTTONDOWN:) i zapamiętane w zmiennej przycisk_wdol. Jeżeli zmienna ta ma wartość True, pobieramy współrzędne kursora myszy (mouse_x, mouse_y = pygame.mouse.get_pos()) i obliczamy indeksy elementu listy POLE_GRY odpowiadającego klikniętej komórce. Następnie sprawdzamy, który przycisk myszy został naciśnięty; informację tę zapisaliśmy wcześniej za pomocą funkcji event.button w zmiennej przycisk_typ, która przyjmuje wartość 1 (lewy) lub 3 (prawy przycisk myszy), w zależności od klikniętego przycisku ożywiamy lub uśmiercamy komórkę, zapisując odpowiedni stan w liście POLE_GRY.

Naciśnięcie klawisza ENTER uruchamia symulację rozwoju populacji. Zmienna zycie_trwa ustawiona zostaje na wartość True , co przerywa obsługę kliknięć myszą, i wywoływana jest funkcja przygotuj_populacje(), która przygotowuje kolejny stan populacji. Końcowe polecenia wypełniają okno gry kolorem (.fill()), wywołują funkcję rysującą planszę (rysuj_populacje()). Funkcja pygame.display.update(), która musi być wykonywana na końcu rysowania, aktualizuje obraz gry na ekranie. Ostatnie polecenie pygame.time.delay(100) dodaje 100-milisekundowe opóźnienie kolejnej aktualizacji stanu populacji. Dzięki temu możemy obserwować jej rozwój na planszy.

Grę możemy uruchomić poleceniem wpisanym w terminalu:

~$ python life_str.py

4.5.4. Zadania dodatkowe

Spróbuj inaczej zaimplementować funkcję przygotuj_populacje. Spróbuj zmodyfikować kod tak, aby plansza gry była biała, a komórki rysowane były jako kolorowe kwadraty o różniącym się od wypełnienia obramowaniu.

4.5.5. Materiały

Źródła:


Licencja Creative Commons 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”