4.4. Kółko i krzyżyk (obj)
Klasyczna gra w kółko i krzyżyk zrealizowana przy pomocy PyGame.

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.
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
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: