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 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 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 120 121 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 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | # coding=utf-8
# Copyright 2014 Janusz Skonieczny
"""
Gra w kółko i krzyżyk
"""
import pygame
import pygame.locals
import logging
# Konfiguracja modułu logowania, element dla zaawansowanych
logging_format = '%(asctime)s %(levelname)-7s | %(module)s.%(funcName)s - %(message)s'
logging.basicConfig(level=logging.DEBUG, format=logging_format, datefmt='%H:%M:%S')
logging.getLogger().setLevel(logging.INFO)
class Board(object):
"""
Plansza do gry. Odpowiada za rysowanie okna gry.
"""
def __init__(self, width):
"""
Konstruktor planszy do gry. Przygotowuje okienko gry.
:param width: szerokość w pikselach
"""
self.surface = pygame.display.set_mode((width, width), 0, 32)
pygame.display.set_caption('Tic-tac-toe')
# Przed pisaniem tekstów, musimy zainicjować mechanizmy wyboru fontów PyGame
pygame.font.init()
font_path = pygame.font.match_font('arial')
self.font = pygame.font.Font(font_path, 48)
# tablica znaczników 3x3 w formie listy
self.markers = [None] * 9
def draw(self, *args):
"""
Rysuje okno gry
:param args: lista obiektów do narysowania
"""
background = (0, 0, 0)
self.surface.fill(background)
self.draw_net()
self.draw_markers()
self.draw_score()
for drawable in args:
drawable.draw_on(self.surface)
# dopiero w tym miejscu następuje fatyczne rysowanie
# w oknie gry, wcześniej tylko ustalaliśmy co i jak ma zostać narysowane
pygame.display.update()
def draw_net(self):
"""
Rysuje siatkę linii na planszy
"""
color = (255, 255, 255)
width = self.surface.get_width()
for i in range(1, 3):
pos = width / 3 * i
# linia pozioma
pygame.draw.line(self.surface, color, (0, pos), (width, pos), 1)
# linia pionowa
pygame.draw.line(self.surface, color, (pos, 0), (pos, width), 1)
def player_move(self, x, y):
"""
Ustawia na planszy znacznik gracza X na podstawie współrzędnych w pikselach
"""
cell_size = self.surface.get_width() / 3
x /= cell_size
y /= cell_size
self.markers[int(x) + int(y) * 3] = player_marker(True)
def draw_markers(self):
"""
Rysuje znaczniki graczy
"""
box_side = self.surface.get_width() / 3
for x in range(3):
for y in range(3):
marker = self.markers[x + y * 3]
if not marker:
continue
# zmieniamy współrzędne znacznika
# na współrzędne w pikselach dla centrum pola
center_x = x * box_side + box_side / 2
center_y = y * box_side + box_side / 2
self.draw_text(self.surface, marker, (center_x, center_y))
def draw_text(self, surface, text, center, color=(180, 180, 180)):
"""
Rysuje wskazany tekst we wskazanym miejscu
"""
text = self.font.render(text, True, color)
rect = text.get_rect()
rect.center = center
surface.blit(text, rect)
def draw_score(self):
"""
Sprawdza czy gra została skończona i rysuje właściwy komunikat
"""
if check_win(self.markers, True):
score = u"Wygrałeś(aś)"
elif check_win(self.markers, True):
score = u"Przegrałeś(aś)"
elif None not in self.markers:
score = u"Remis!"
else:
return
i = self.surface.get_width() / 2
self.draw_text(self.surface, score, center=(i, i), color=(255, 26, 26))
class TicTacToeGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, ai_turn=False):
"""
Przygotowanie ustawień gry
:param width: szerokość planszy mierzona w pikselach
"""
pygame.init()
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.board = Board(width)
self.ai = Ai(self.board)
self.ai_turn = ai_turn
def run(self):
"""
Główna pętla gry
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.board.draw()
if self.ai_turn:
self.ai.make_turn()
self.ai_turn = False
self.fps_clock.tick(15)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
if event.type == pygame.locals.MOUSEBUTTONDOWN:
if self.ai_turn:
# jeśli jeszcze trwa ruch komputera to ignorujemy zdarzenia
continue
# pobierz aktualną pozycję kursora na planszy mierzoną w pikselach
x, y = pygame.mouse.get_pos()
self.board.player_move(x, y)
self.ai_turn = True
class Ai(object):
"""
Kieruje ruchami komputera na podstawie analizy położenia znaczników
"""
def __init__(self, board):
self.board = board
def make_turn(self):
"""
Wykonuje ruch komputera
"""
if not None in self.board.markers:
# brak dostępnych ruchów
return
logging.debug("Plansza: %s" % self.board.markers)
move = self.next_move(self.board.markers)
self.board.markers[move] = player_marker(False)
@classmethod
def next_move(cls, markers):
"""
Wybierz następny ruch komputera na podstawie wskazanej planszy
:param markers: plansza gry
:return: index tablicy jednowymiarowe w której należy ustawić znacznik kółka
"""
# pobierz dostępne ruchy wraz z oceną
moves = cls.score_moves(markers, False)
# wybierz najlepiej oceniony ruch
score, move = max(moves, key=lambda m: m[0])
logging.info("Dostępne ruchy: %s", moves)
logging.info("Wybrany ruch: %s %s", move, score)
return move
@classmethod
def score_moves(cls, markers, x_player):
"""
Ocenia rekurencyjne możliwe ruchy
Jeśli ruch jest zwycięstwem otrzymuje +1, jeśli przegraną -1
lub 0 jeśli nie nie ma zwycięscy. Dla ruchów bez zwycięscy rekreacyjnie
analizowane są kolejne ruchy a suma ich punktów jest wynikiem aktualnego
ruchu.
:param markers: plansza na podstawie której analizowane są następne ruchy
:param x_player: True jeśli ruch dotyczy gracza X, False dla gracza O
"""
# wybieramy wszystkie możliwe ruchy na podstawie wolnych pól
available_moves = (i for i, m in enumerate(markers) if m is None)
for move in available_moves:
from copy import copy
# tworzymy kopię planszy która na której testowo zostanie
# wykonany ruch w celu jego późniejszej oceny
proposal = copy(markers)
proposal[move] = player_marker(x_player)
# sprawdzamy czy ktoś wygrywa gracz którego ruch testujemy
if check_win(proposal, x_player):
# dodajemy punkty jeśli to my wygrywamy
# czyli nie x_player
score = -1 if x_player else 1
yield score, move
continue
# ruch jest neutralny,
# sprawdzamy rekurencyjne kolejne ruchy zmieniając gracza
next_moves = list(cls.score_moves(proposal, not x_player))
if not next_moves:
yield 0, move
continue
# rozdzielamy wyniki od ruchów
scores, moves = zip(*next_moves)
# sumujemy wyniki możliwych ruchów, to będzie nasz wynik
yield sum(scores), move
def player_marker(x_player):
"""
Funkcja pomocnicza zwracająca znaczniki graczy
:param x_player: True dla gracza X False dla gracza O
:return: odpowiedni znak gracza
"""
return "X" if x_player else "O"
def check_win(markers, x_player):
"""
Sprawdza czy przekazany zestaw znaczników gry oznacza zwycięstwo wskazanego gracza
:param markers: jednowymiarowa sekwencja znaczników w
:param x_player: True dla gracza X False dla gracza O
"""
win = [player_marker(x_player)] * 3
seq = range(3)
# definiujemy funkcję pomocniczą pobierającą znacznik
# na podstawie współrzędnych x i y
def marker(xx, yy):
return markers[xx + yy * 3]
# sprawdzamy każdy rząd
for x in seq:
row = [marker(x, y) for y in seq]
if row == win:
return True
# sprawdzamy każdą kolumnę
for y in seq:
col = [marker(x, y) for x in seq]
if col == win:
return True
# sprawdzamy przekątne
diagonal1 = [marker(i, i) for i in seq]
diagonal2 = [marker(i, abs(i-2)) for i in seq]
if diagonal1 == win or diagonal2 == win:
return True
# Ta część powinna być zawsze na końcu modułu (ten plik jest modułem)
# chcemy uruchomić naszą grę dopiero po tym jak wszystkie klasy zostaną zadeklarowane
if __name__ == "__main__":
game = TicTacToeGame(300)
game.run()
|
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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |