Kompletny program: gra w warcaby ================================ Przykłady, które pojawiały się do tej pory, były z reguły dość proste. Można by odnieść wrażenie, że prościej było by je napisać bez obiektów. Zalety obiektowości ujawniają się w przypadku większych projektów; gra w warcaby wciąż jest dośc prostym programem. W mniej niż 300 liniach kodu udało się zmieścić program z obowiązkowym biciem i damkami. Jedyne uproszczenia to: - w jednym ruchu możliwe jest tylko jedno bicie - damka musi zakończyć ruch na polu bezpośrednio za zbitym pionem W gotową grę możesz zagrać tu: .. raw:: html :file: play_checkers.html Poniżej zaś znajduje się opis programu a na samym dole tej strony - kompletny kod. Pionki, ich położenie i kierunek ruchu --------------------------------------- Zastanówmy się najpierw, jakich obiektów będziemy potrzebować. W prawdziwej grze udział biorą *gracze*, *pionki* i *szachownica*. Dodatkowo jakoś trzeba zaimplementować reguły gry. Każdy z tych elementów będzie oddzielną klasą. Zacznijmy od reguł, bo one powodują najwięcej komplikacji (np. damka rusza się inaczej niż zwykły pionek). Dodatkowo, zmiana sposobu implementacji tych reguł pociąga za sobą konieczność przebudowania większości programu. Sprawdzenie kilku potencjalnych możliwości pokazało, że najlepiej będzie, kiedy to *pionek* będzie wiedział, gdzie może się ruszyć. Oprócz zwykłych pionków będą się też pojawiać *damki* - obiekty kalsy ``Queen``, które będą poruszały się wg innych reguł. Pionek koniecznie musi wiedzieć, jakiego jest koloru: biały, czy czarny. Klasa bazowa ``PieceBase`` wyglądać będzie zatem nastepująco: .. literalinclude:: checkers2.py :language: python :lines: 59-67 Zauważ, że aby metoda ``move_proposals()`` mogła wymienić wszystkie dostępne prawidłowe ruchy, potrzebne jest położenie tego pionka na planszy oraz położenie wszystkich innych pionków. Potrzebny jest też kolor ruszanego pionka, ale tą informację pionek sam przechowuje już jako pole prywatne. Z położeniem sytuacja jest bardziej skomplikowana, bo informacja ta przydała by się w 2 miejscach: 1. pionek powinien znać swoje położenie 2. szachownica powinna przechowywać wszystkie pionki, a zatem znać ich położenia Reguły programowania obiektowego wymagają, aby informacja o położeniu przechowywana była tylko w jednym miejscu; inaczej mógłby się pojawić problem jego z aktualizowaniem. Wybieramy zatem opcję 2: położenia wszystkich pionków trzyma szachownica. W tej sytuacji obiekt klasy ``Board`` trzeba przekazać metodzie ``move_proposals()`` aby mogła zaproponować możliwe ruchy. Do zapisywania pozycji pionków stworzymy specjalną klasę - ``Location``. Na pierwszy rzut oka ktoś mógłby zarzucić, że to nadmiar szcześcia. Położenie na szachownicy jest jednak dwuwymiarowe ido tego potrzebna jest jakaś struktura danych. Można oczywiście użyć listy lub krotki; dedykowaną klasę możemy jednak wyposażyć w dodatkowe metody, jak np. drukowanie położenia w notacji szachowej (*pionek z C2 na D3*). W tym celu ``Location`` trzyma prywatne pole - listę liter użytych do numerowania linii szachownicy: .. literalinclude:: checkers2.py :language: python :lines: 33-48 Przyda się też klasa do zapisywania kierunku ruchu; ``Direction`` to w zasadzie wektor przesunięcia pionka na szachownicy: .. literalinclude:: checkers2.py :language: python :lines: 16-30 Od razu zdefiniowaliśmy wszystkie możliwe kierunki: ``UR``, ``UL``, ``DR`` i ``DL``. Przechowane są jako *pola klasy* ``Direction``. Teraz możemy dodać do klasy ``Location`` metodę przesuwającą położenie o zadany wektor. Zauważ, że *publiczna* metoda ``move_by()`` akceptuje argument typu ``Direction``; obliczeniami zajmuje się metoda prywatna. .. literalinclude:: checkers2.py :language: python :lines: 33-34,50-56 Jak już wspomniano, pionek musi mieć swój kolor. Informację tę można przechowywać na wiele sposobów: można użyć napisów ``"BLACK"`` i ``"WHITE"`` albo umówić się, że czarne to 0 a białe to 1. Rozwiązania te mogą jednak powodować niejednoznaczności; ktoś mógłby pomylić 0 z 1, albo napisać ``"White"``. Aby uniknąć problemów, użyjemy typu ``Enum`` (*enumeration*, czyli wyliczenie). Jest to klasa, która zawiera pola - konkretne wartości: .. literalinclude:: checkers2.py :language: python :lines: 5,6,8-14 Jak widać na powyższym przykładzie, w języku Python wyliczenie jest klasą, która dziedziczy po typie bazowym ``Enum`` i jak każda klasa - może mieć metody. Tu dodaliśmy statyczną metodę, która zwraca kolor przeciwnika. **Podsumowując**: stworzyliśmy klasę bazową dla pionków (``PieceBase``), z której w przyszłości wyprowadzimy klasy pionka i damki. Najważniejszą funkcjonalnością pionka jest metoda ``move_proposals()``, która na podstawie aktualnego stanu planszy zwróci listę dozwolonych ruchów. Stworzyliśmy też pomocnicze klasy, które przechowują potrzebne nam informacje: położenie (``Location``), kierunek ruchu pionka (``Direction``) i jego kolor (``Color``). Szachownica ------------ Programowanie obiektowe wymaga podejmowania decyzji *"kto co robi"*, np. jakie będą obowiązki pionka a jakie gracza, oraz *"kto co ma*", np. kto zawiera obiekty reprezentujący pionki: szachownica, czy może gracz. W tym przypadku zdecydowałem, że to szachownica zawiera pionki. A zatem klasa ``Board``: - wie, które pola są czarne a które białe `(wzór z Wikipedii) `__ - umie zwrócić listę wszystkich lokacji dostęnych dla pionków (metoda ``locations()``) - w warcaby gramy jedynie na czarnych polach! - posiada pionki; w tym celu powinna posiadać jako składową *jakiś* pojemnik - słownik albo listę. Którą z nich wybrać? Na pewno można zastosować zarówno jedną jak i drugą; ja zdecydopwałem się na listę. Ponieważ szachownica jest 2D, potrzebna będzie lista list. Dodatkowo powinniśmy konsekwentnie stosować klasę ``Location``: szachownica powinna umieć powiedzieć, co stoi na polu ``C3``. - Szachownica powinna wykonywać ruch. Co prawda w prawdziwej partii robi to gracz, ale w aktualnej implementacji tylko szachownica zna położenia wszystkich pionków i to ona powinna usuwać zbite. - Dodatkowo szachownica musi zwracać listę pionków danego koloru wraz z ich położeniami; inaczej gracz nie będzie mógł zrobić ruchu. - Konstruktor klasy ``Board`` umożlwia grę na innych planszach niż 8x8 i z inną niż standardowa liczą pionów. .. literalinclude:: checkers2.py :language: python :lines: 145-198 Ruchy pionków --------------- Generalnie przebieg pojedynczego ruchu jednego gracza będzie następujący: - Gracz pobiera z planszy *swoje* pionki, wywołując ``Board.pieces()`` - Gracz odpytuje każdy z pionków o jego ruchy, wywołując ``AbstractPiece.move_proposals()``; każdy pionek zwraca listę możliwych ruchów a gracz gromadzi je w liście; do reprezentowania ruchu potrzebna będzie klasa ``Move`` - Mając listę wszystkich możliwych ruchów, gracz podejmuje decyzję, wybierając najlepszy wg niego obiekt klasy ``Move``. - Wykonuje ruch, wywołując metodę ``Board.make_move(self, m:Move)`` Klasa ``Move`` przechowuje początkową i końcową pozycję ruchu a także ewentualną pozycję zbijanego piona. Dodatkowo klasa ta posiada metodę ``__str__()``, wypisującą ruch na ekranie. .. literalinclude:: checkers2.py :language: python :lines: 73-89 Mając obiekt ``Move``, jego wykonanie jest proste: szachownica musi przesunąć ruszany pionek i ewentualnie usunąć ten zbijany. Dodatkowo jednak musimy spradzić, czy pionek nie jest promowany do damki: dzieję się to wtedy, kiedy biały pion dojdzie linii ostatniej (najczęściej 8) albo czarny do linii 1. .. literalinclude:: checkers2.py :language: python :lines: 144,182-191 Pozostała zatem do napisania ta część programu, w której proponowane są ruchy: .. literalinclude:: checkers2.py :language: python :lines: 91,98-114 Algorytm jest stosunkowo prosty: - Jeżeli pionek jest czarny, to możliwy jest ruch w kierunku ``BL`` i ``BR`` (Bottom-Left i Bottom-Right), jeżeli biały zaś, to ``UL`` i ``UR``; zakładamy bowiem, że białe zaczynają na dole a czarne na górze planszy - Następnie sprawdzamy, czy pole docelowe po wykonaniu ruchu jest wolne; jeżeli tak, to ruch trafia na listę. Jeżeli jednak nie jest wolne, to sprawdzamy, czy wolne jest następne pole w tym kierunku i czy przeskakujemy pionek przeciwnika. Jeżeli tak, to mamy bicie! - Zauważ, że nie sprawdzamy tu, czy aby ruch nie wychodzi poza planszę - tym zajmie się klasa ``Board`` Mamy jednak jeszcze jedną klasę dziedziczącą po ``PieceBase`` - klasę damka (``Queen``). W tym przypadku generowanie ruchów jest bardziej skomplikowane; pełen kod można znaleźć poniżej. Kod programu --------------------------------------- W około 250 liniach programu udało się zawrzeć kompletną grę w warcaby z damkami i obowiązkowym biciem. Klasa ``Board`` umie narysować stan gry w terminalu, można więc śledzić rozgrywkę. Jedyne poważne mankamenty gry to, wspomniane już na wstępie, to: - Pionek może wykonać tylko jedno bicie - Damka również może zbić tylko jeden pionek i ląduje wtedy na polu bezpośrednio następującym po zbitym pionie .. literalinclude:: checkers2.py :language: python :linenos: Dodatek: Interfejs graficzny gry --------------------------------------- Powyższy program gra sam ze sobą. Można nim np sprawdzić, ile średnio ruchów potrzeba, aby rozegrać partię, albo czy białe wygrywają częściej niż czarne. Aby jednak *samemu zagrać w warcaby*, potrzebny jest interfejs graficzny. Napiszemy go również w Pythonie; dzięki bibliotece brython.js będzie on działał w przeglądace na komputerze użytkownika. .. rubric:: Rysowanie planszy Klasa ``CheckersGui`` rysuje planszę w oknie przeglądarki; wykorzystuje do tego bibliotekę graficzną VisuaLife. Metody ``draw_piece()`` oraz ``draw_checkerboard()`` rysują (odpowiednio): pionki oraz szachownicę. Metody te podpinają też zdarzenia - procedury wywoływane przez przeglądarkę w momencie, kiedy gracz kliknie na pole lub pionek. Dla działania gry kluczowe jest połączenie obiektów gry z ich graficzną reprezentacją na ekranie. Wykorzystujemy tu tekstową reprezentację opisanej powyżej klasy ``Location``, np ``B3`` lub ``C2``. Każdy z elementów narysowanych na stronie (np kółko reprezentujace pionek) ma unikalny identyfikator, w którym ostatnie dwa znaki to właśnie owa lokaliozacja. Tworzenie owych unikalnych identyfikatorów wygląda następująco: .. literalinclude:: play_checkers.py :language: python :lines: 38-41 Przy odczytywaniu zaś pobieramy cel (*target*) zdarzenia (obiekt ``evt``, który jest elementem strony), jego ``id`` zawiera szukany napis: .. literalinclude:: play_checkers.py :language: python :lines: 24 .. rubric:: Pętla gry Drugim ważnym elementem interfejsu jest główna pętla gry. W programie opisanym powyżej (linie 247-256 sekcji ``__main__``) mamy nieskończoną pętlę. Rozwiązanie takie nie powinno być stosowane w GUI gdyż zablokowało by przeglądarkę. W tym przypadku mamy trzy niezależne funkcje, które działają równolegle: ``play_with_gui()``, ``move_start(evt)`` oraz ``move_end()``. Dwie ostatnie uruchamiane są przez zdarzenia, kiedy użytkownik kliknie na pionek lub pole planszy. Ich efektem jest zapisanie odpowiedniej lokacji w prywatnym polu klasy ``CheckersGui``. Funkcja ``play_with_gui()`` zaś jest uruchamiana cyklicznie co 300 milisekund dzięki modułowi ``timer``: .. literalinclude:: play_checkers.py :language: python :lines: 135 Po zakończeniu gry wywołania kończy instrukcja: ``timer.clear_interval(loop_handler)`` (linia 112). A oto pełen kod gry interaktywnej: .. literalinclude:: play_checkers.py :language: python :linenos: