4.4. Kółko i krzyżyk (obj)

Klasyczna gra w kółko i krzyżyk zrealizowana przy pomocy PyGame.

../../_images/screen11.png

4.4.1. Okienko gry

Na wstępie w pliku ~/python101/games/tic_tac_toe.py otrzymujemy kod który przygotuje okienko naszej gry:

Informacja

Ten przykład zakłada wcześniejsze zrealizowanie przykładu: Życie Conwaya (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.

Ostrzeżenie

TODO: Wymaga ewentualnego rozbicia i uzupełnienia opisów.

Kod nr
  1# coding=utf-8
  2# Copyright 2014 Janusz Skonieczny
  3
  4"""
  5Gra w kółko i krzyżyk
  6"""
  7
  8import pygame
  9import pygame.locals
 10import logging
 11
 12# Konfiguracja modułu logowania, element dla zaawansowanych
 13logging_format = '%(asctime)s %(levelname)-7s | %(module)s.%(funcName)s - %(message)s'
 14logging.basicConfig(level=logging.DEBUG, format=logging_format, datefmt='%H:%M:%S')
 15logging.getLogger().setLevel(logging.INFO)
 16
 17
 18class Board(object):
 19    """
 20    Plansza do gry. Odpowiada za rysowanie okna gry.
 21    """
 22
 23    def __init__(self, width):
 24        """
 25        Konstruktor planszy do gry. Przygotowuje okienko gry.
 26
 27        :param width: szerokość w pikselach
 28        """
 29        self.surface = pygame.display.set_mode((width, width), 0, 32)
 30        pygame.display.set_caption('Tic-tac-toe')
 31
 32        # Przed pisaniem tekstów, musimy zainicjować mechanizmy wyboru fontów PyGame
 33        pygame.font.init()
 34        font_path = pygame.font.match_font('arial')
 35        self.font = pygame.font.Font(font_path, 48)
 36
 37        # tablica znaczników 3x3 w formie listy
 38        self.markers = [None] * 9
 39
 40    def draw(self, *args):
 41        """
 42        Rysuje okno gry
 43
 44        :param args: lista obiektów do narysowania
 45        """
 46        background = (0, 0, 0)
 47        self.surface.fill(background)
 48        self.draw_net()
 49        self.draw_markers()
 50        self.draw_score()
 51        for drawable in args:
 52            drawable.draw_on(self.surface)
 53
 54        # dopiero w tym miejscu następuje fatyczne rysowanie
 55        # w oknie gry, wcześniej tylko ustalaliśmy co i jak ma zostać narysowane
 56        pygame.display.update()
 57
 58    def draw_net(self):
 59        """
 60        Rysuje siatkę linii na planszy
 61        """
 62        color = (255, 255, 255)
 63        width = self.surface.get_width()
 64        for i in range(1, 3):
 65            pos = width / 3 * i
 66            # linia pozioma
 67            pygame.draw.line(self.surface, color, (0, pos), (width, pos), 1)
 68            # linia pionowa
 69            pygame.draw.line(self.surface, color, (pos, 0), (pos, width), 1)
 70
 71    def player_move(self, x, y):
 72        """
 73        Ustawia na planszy znacznik gracza X na podstawie współrzędnych w pikselach
 74        """
 75        cell_size = self.surface.get_width() / 3
 76        x /= cell_size
 77        y /= cell_size
 78        self.markers[int(x) + int(y) * 3] = player_marker(True)
 79
 80    def draw_markers(self):
 81        """
 82        Rysuje znaczniki graczy
 83        """
 84        box_side = self.surface.get_width() / 3
 85        for x in range(3):
 86            for y in range(3):
 87                marker = self.markers[x + y * 3]
 88                if not marker:
 89                    continue
 90                # zmieniamy współrzędne znacznika
 91                # na współrzędne w pikselach dla centrum pola
 92                center_x = x * box_side + box_side / 2
 93                center_y = y * box_side + box_side / 2
 94
 95                self.draw_text(self.surface, marker, (center_x, center_y))
 96
 97    def draw_text(self, surface,  text, center, color=(180, 180, 180)):
 98        """
 99        Rysuje wskazany tekst we wskazanym miejscu
100        """
101        text = self.font.render(text, True, color)
102        rect = text.get_rect()
103        rect.center = center
104        surface.blit(text, rect)
105
106    def draw_score(self):
107        """
108        Sprawdza czy gra została skończona i rysuje właściwy komunikat
109        """
110        if check_win(self.markers, True):
111            score = u"Wygrałeś(aś)"
112        elif check_win(self.markers, True):
113            score = u"Przegrałeś(aś)"
114        elif None not in self.markers:
115            score = u"Remis!"
116        else:
117            return
118
119        i = self.surface.get_width() / 2
120        self.draw_text(self.surface, score, center=(i, i), color=(255, 26, 26))
121
122
123class TicTacToeGame(object):
124    """
125    Łączy wszystkie elementy gry w całość.
126    """
127
128    def __init__(self, width, ai_turn=False):
129        """
130        Przygotowanie ustawień gry
131        :param width: szerokość planszy mierzona w pikselach
132        """
133        pygame.init()
134        # zegar którego użyjemy do kontrolowania szybkości rysowania
135        # kolejnych klatek gry
136        self.fps_clock = pygame.time.Clock()
137
138        self.board = Board(width)
139        self.ai = Ai(self.board)
140        self.ai_turn = ai_turn
141
142    def run(self):
143        """
144        Główna pętla gry
145        """
146        while not self.handle_events():
147            # działaj w pętli do momentu otrzymania sygnału do wyjścia
148            self.board.draw()
149            if self.ai_turn:
150                self.ai.make_turn()
151                self.ai_turn = False
152            self.fps_clock.tick(15)
153
154    def handle_events(self):
155        """
156        Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
157
158        :return True jeżeli pygame przekazał zdarzenie wyjścia z gry
159        """
160        for event in pygame.event.get():
161            if event.type == pygame.locals.QUIT:
162                pygame.quit()
163                return True
164
165            if event.type == pygame.locals.MOUSEBUTTONDOWN:
166                if self.ai_turn:
167                    # jeśli jeszcze trwa ruch komputera to ignorujemy zdarzenia
168                    continue
169                # pobierz aktualną pozycję kursora na planszy mierzoną w pikselach
170                x, y = pygame.mouse.get_pos()
171                self.board.player_move(x, y)
172                self.ai_turn = True
173
174
175class Ai(object):
176    """
177    Kieruje ruchami komputera na podstawie analizy położenia znaczników
178    """
179    def __init__(self, board):
180        self.board = board
181
182    def make_turn(self):
183        """
184        Wykonuje ruch komputera
185        """
186        if not None in self.board.markers:
187            # brak dostępnych ruchów
188            return
189        logging.debug("Plansza: %s" % self.board.markers)
190        move = self.next_move(self.board.markers)
191        self.board.markers[move] = player_marker(False)
192
193    @classmethod
194    def next_move(cls, markers):
195        """
196        Wybierz następny ruch komputera na podstawie wskazanej planszy
197        :param markers: plansza gry
198        :return: index tablicy jednowymiarowe w której należy ustawić znacznik kółka
199        """
200        # pobierz dostępne ruchy wraz z oceną
201        moves = cls.score_moves(markers, False)
202        # wybierz najlepiej oceniony ruch
203        score, move = max(moves, key=lambda m: m[0])
204        logging.info("Dostępne ruchy: %s", moves)
205        logging.info("Wybrany ruch: %s %s", move, score)
206        return move
207
208    @classmethod
209    def score_moves(cls, markers, x_player):
210        """
211        Ocenia rekurencyjne możliwe ruchy
212
213        Jeśli ruch jest zwycięstwem otrzymuje +1, jeśli przegraną -1
214        lub 0 jeśli nie nie ma zwycięscy. Dla ruchów bez zwycięscy rekreacyjnie
215        analizowane są kolejne ruchy a suma ich punktów jest wynikiem aktualnego
216        ruchu.
217
218        :param markers: plansza na podstawie której analizowane są następne ruchy
219        :param x_player: True jeśli ruch dotyczy gracza X, False dla gracza O
220        """
221        # wybieramy wszystkie możliwe ruchy na podstawie wolnych pól
222        available_moves = (i for i, m in enumerate(markers) if m is None)
223        for move in available_moves:
224            from copy import copy
225            # tworzymy kopię planszy która na której testowo zostanie
226            # wykonany ruch w celu jego późniejszej oceny
227            proposal = copy(markers)
228            proposal[move] = player_marker(x_player)
229
230            # sprawdzamy czy ktoś wygrywa gracz którego ruch testujemy
231            if check_win(proposal, x_player):
232                # dodajemy punkty jeśli to my wygrywamy
233                # czyli nie x_player
234                score = -1 if x_player else 1
235                yield score, move
236                continue
237
238            # ruch jest neutralny,
239            # sprawdzamy rekurencyjne kolejne ruchy zmieniając gracza
240            next_moves = list(cls.score_moves(proposal, not x_player))
241            if not next_moves:
242                yield 0, move
243                continue
244
245            # rozdzielamy wyniki od ruchów
246            scores, moves = zip(*next_moves)
247            # sumujemy wyniki możliwych ruchów, to będzie nasz wynik
248            yield sum(scores), move
249
250
251def player_marker(x_player):
252    """
253    Funkcja pomocnicza zwracająca znaczniki graczy
254    :param x_player: True dla gracza X False dla gracza O
255    :return: odpowiedni znak gracza
256    """
257    return "X" if x_player else "O"
258
259
260def check_win(markers, x_player):
261    """
262    Sprawdza czy przekazany zestaw znaczników gry oznacza zwycięstwo wskazanego gracza
263
264    :param markers: jednowymiarowa sekwencja znaczników w
265    :param x_player: True dla gracza X False dla gracza O
266    """
267    win = [player_marker(x_player)] * 3
268    seq = range(3)
269
270    # definiujemy funkcję pomocniczą pobierającą znacznik
271    # na podstawie współrzędnych x i y
272    def marker(xx, yy):
273        return markers[xx + yy * 3]
274
275    # sprawdzamy każdy rząd
276    for x in seq:
277        row = [marker(x, y) for y in seq]
278        if row == win:
279            return True
280
281    # sprawdzamy każdą kolumnę
282    for y in seq:
283        col = [marker(x, y) for x in seq]
284        if col == win:
285            return True
286
287    # sprawdzamy przekątne
288    diagonal1 = [marker(i, i) for i in seq]
289    diagonal2 = [marker(i, abs(i-2)) for i in seq]
290    if diagonal1 == win or diagonal2 == win:
291        return True
292
293
294# Ta część powinna być zawsze na końcu modułu (ten plik jest modułem)
295# chcemy uruchomić naszą grę dopiero po tym jak wszystkie klasy zostaną zadeklarowane
296if __name__ == "__main__":
297    game = TicTacToeGame(300)
298    game.run()
299

W powyższym kodzie mamy podstawy potrzebne do uruchomienia gry:

~/python101$ python games/tic_tac_toe.py

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:

2025-04-12 o 10:21 w Sphinx 7.3.7

Autorzy:

Patrz plik „Autorzy”