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:
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:
class PieceBase(ABC):
def __init__(self, color:Color):
self.__color = color
@property
def color(self) -> Color: return self.__color
@abstractmethod
def move_proposals(self, loc:Location, brd): pass
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:
pionek powinien znać swoje położenie
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:
class Location:
__letters = list("_ABCDEFGHIJKLMNOP")
board_size = 8
def __init__(self, row: int, col: int):
self.__row, self.__col = row, col
def __str__(self):
return "%c%d" % (Location.__letters[self.__row], self.__col)
@property
def row(self) -> int: return self.__row
@property
def col(self) -> int: return self.__col
Przyda się też klasa do zapisywania kierunku ruchu; Direction to w zasadzie wektor przesunięcia pionka na szachownicy:
class Direction:
def __init__(self,dr,dc):
self.__dr = dr
self.__dc = dc
@property
def dr(self): return self.__dr
@property
def dc(self): return self.__dc
Direction.UR = Direction(1,1)
Direction.UL = Direction(1,-1)
Direction.BR = Direction(-1,1)
Direction.BL = Direction(-1,-1)
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.
class Location:
def move_by(self, d: Direction, n: int = 1):
return self.__move_by_vector(d.dr * n, d.dc * n)
def __move_by_vector(self, dr, dc):
r, c = self.__row + dr, self.__col + dc
if any([r < 1, r > Location.board_size, c < 1, c > Location.board_size]): return None
return Location(r, c)
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:
from enum import Enum
class Color(Enum):
BLACK = 1
WHITE = 2
@staticmethod
def enemy_color(col):
return Color.WHITE if col==Color.BLACK else Color.BLACK
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 poluC3.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
Boardumożlwia grę na innych planszach niż 8x8 i z inną niż standardowa liczą pionów.
def __init__(self, size=8, n_pieces=12):
# should check if size is even!
Location.board_size = size
self.__size = size
self.__b = [[None for _ in range(size+1)] for _ in range(size+1)]
blacks = self.locations(Color.BLACK)
if n_pieces > 0:
for pos in blacks[:n_pieces]:
self[pos] = Piece(Color.WHITE)
for pos in blacks[-n_pieces:]:
self[pos] = Piece(Color.BLACK)
def __getitem__(self, loc:Location):
return self.__b[loc.row][loc.col]
def __setitem__(self, loc:Location, item: Piece):
self.__b[loc.row][loc.col] = item
def pieces(self, color:Color) -> list[tuple[Location, PieceBase]]:
ret = []
for r in range(1, self.__size+1):
for c in range(1, self.__size + 1):
piece = self.__b[r][c]
if piece is not None and piece.color == color:
ret.append((Location(r, c), piece))
return ret
def locations(self, color: Color):
all_loc = []
for row in range(1, self.__size + 1):
for col in range(1, self.__size + 1):
if l := Location(row, col):
if Board.color(l) == color:
all_loc.append(l)
return all_loc
def make_move(self, m:Move):
piece: PieceBase = self[m.start]
self[m.start] = None
self[m.stop] = piece
if piece.color == Color.BLACK and m.stop.row == 1:
self[m.stop] = Queen(Color.BLACK)
elif piece.color == Color.WHITE and m.stop.row == self.__size:
self[m.stop] = Queen(Color.WHITE)
if m.captured is not None:
self[m.captured]=None
@staticmethod
def color(loc:Location):
if (loc.col + loc.row) % 2 == 0:
return Color.BLACK
else:
return Color.WHITE
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 klasaMoveMają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.
class Move:
def __init__(self, start: Location, stop: Location, captured: Location = None):
self.__start = start
self.__stop = stop
self.__captured = captured
@property
def start(self): return self.__start
@property
def stop(self): return self.__stop
@property
def captured(self): return self.__captured
def __str__(self):
return str(self.__start)+" -> "+str(self.__stop)
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.
class Board:
def make_move(self, m:Move):
piece: PieceBase = self[m.start]
self[m.start] = None
self[m.stop] = piece
if piece.color == Color.BLACK and m.stop.row == 1:
self[m.stop] = Queen(Color.BLACK)
elif piece.color == Color.WHITE and m.stop.row == self.__size:
self[m.stop] = Queen(Color.WHITE)
if m.captured is not None:
self[m.captured]=None
Pozostała zatem do napisania ta część programu, w której proponowane są ruchy:
class Piece(PieceBase):
def move_proposals(self, loc: Location, brd) -> list[Move]:
moves = []
if self.color == Color.BLACK:
dirs = [Direction.BL, Direction.BR]
else:
dirs = [Direction.UL, Direction.UR]
for d in dirs:
if l:=loc.move_by(d): # --- l==None means the move went outside the board
if brd[l] is None: moves.append(Move(loc, l))
elif brd[l].color == Color.enemy_color(self.color):
if ll := l.move_by(d):
if brd[ll] is None:
moves.append(Move(loc, ll, l))
return moves
Algorytm jest stosunkowo prosty:
Jeżeli pionek jest czarny, to możliwy jest ruch w kierunku
BLiBR(Bottom-Left i Bottom-Right), jeżeli biały zaś, toULiUR; zakładamy bowiem, że białe zaczynają na dole a czarne na górze planszyNastę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
1from __future__ import annotations
2
3import random
4from abc import ABC, abstractmethod
5from enum import Enum
6
7
8class Color(Enum):
9 BLACK = 1
10 WHITE = 2
11
12 @staticmethod
13 def enemy_color(col):
14 return Color.WHITE if col==Color.BLACK else Color.BLACK
15
16class Direction:
17 def __init__(self,dr,dc):
18 self.__dr = dr
19 self.__dc = dc
20
21 @property
22 def dr(self): return self.__dr
23
24 @property
25 def dc(self): return self.__dc
26
27Direction.UR = Direction(1,1)
28Direction.UL = Direction(1,-1)
29Direction.BR = Direction(-1,1)
30Direction.BL = Direction(-1,-1)
31
32
33class Location:
34
35 __letters = list("_ABCDEFGHIJKLMNOP")
36 board_size = 8
37
38 def __init__(self, row: int, col: int):
39 self.__row, self.__col = row, col
40
41 def __str__(self):
42 return "%c%d" % (Location.__letters[self.__row], self.__col)
43
44 @property
45 def row(self) -> int: return self.__row
46
47 @property
48 def col(self) -> int: return self.__col
49
50 def move_by(self, d: Direction, n: int = 1):
51 return self.__move_by_vector(d.dr * n, d.dc * n)
52
53 def __move_by_vector(self, dr, dc):
54 r, c = self.__row + dr, self.__col + dc
55 if any([r < 1, r > Location.board_size, c < 1, c > Location.board_size]): return None
56 return Location(r, c)
57
58
59class PieceBase(ABC):
60 def __init__(self, color:Color):
61 self.__color = color
62
63 @property
64 def color(self) -> Color: return self.__color
65
66 @abstractmethod
67 def move_proposals(self, loc:Location, brd): pass
68
69 @abstractmethod
70 def symbol(self): pass
71
72
73class Move:
74 def __init__(self, start: Location, stop: Location, captured: Location = None):
75 self.__start = start
76 self.__stop = stop
77 self.__captured = captured
78
79 @property
80 def start(self): return self.__start
81
82 @property
83 def stop(self): return self.__stop
84
85 @property
86 def captured(self): return self.__captured
87
88 def __str__(self):
89 return str(self.__start)+" -> "+str(self.__stop)
90
91class Piece(PieceBase):
92 def __init__(self, color: Color):
93 super().__init__(color)
94
95 def symbol(self):
96 return '*' if self.color==Color.BLACK else 'o'
97
98 def move_proposals(self, loc: Location, brd) -> list[Move]:
99
100 moves = []
101 if self.color == Color.BLACK:
102 dirs = [Direction.BL, Direction.BR]
103 else:
104 dirs = [Direction.UL, Direction.UR]
105
106 for d in dirs:
107 if l:=loc.move_by(d): # --- l==None means the move went outside the board
108 if brd[l] is None: moves.append(Move(loc, l))
109 elif brd[l].color == Color.enemy_color(self.color):
110 if ll := l.move_by(d):
111 if brd[ll] is None:
112 moves.append(Move(loc, ll, l))
113 return moves
114
115
116class Queen(PieceBase):
117 def __init__(self, color: Color):
118 super().__init__(color)
119
120 def symbol(self):
121 return 'X' if self.color==Color.BLACK else 'O'
122
123 def move_proposals(self, loc: Location, brd):
124
125 moves = []
126 dirs = [Direction.BL, Direction.BR, Direction.UL, Direction.UR]
127
128 for d in dirs:
129 for i in range(1,Location.board_size):
130 l = loc.move_by(d, i)
131 if l is None: break # --- None means the move went outside the board
132 if brd[l] is None:
133 moves.append(Move(loc, l))
134 elif brd[l].color == Color.enemy_color(self.color):
135 if ll := l.move_by(d):
136 if brd[ll] is None:
137 moves.append(Move(loc, ll, l))
138 else:
139 break
140
141 return moves
142
143
144class Board:
145 def __init__(self, size=8, n_pieces=12):
146 # should check if size is even!
147 Location.board_size = size
148 self.__size = size
149 self.__b = [[None for _ in range(size+1)] for _ in range(size+1)]
150 blacks = self.locations(Color.BLACK)
151 if n_pieces > 0:
152 for pos in blacks[:n_pieces]:
153 self[pos] = Piece(Color.WHITE)
154
155 for pos in blacks[-n_pieces:]:
156 self[pos] = Piece(Color.BLACK)
157
158 def __getitem__(self, loc:Location):
159 return self.__b[loc.row][loc.col]
160
161 def __setitem__(self, loc:Location, item: Piece):
162 self.__b[loc.row][loc.col] = item
163
164 def pieces(self, color:Color) -> list[tuple[Location, PieceBase]]:
165 ret = []
166 for r in range(1, self.__size+1):
167 for c in range(1, self.__size + 1):
168 piece = self.__b[r][c]
169 if piece is not None and piece.color == color:
170 ret.append((Location(r, c), piece))
171 return ret
172
173 def locations(self, color: Color):
174 all_loc = []
175 for row in range(1, self.__size + 1):
176 for col in range(1, self.__size + 1):
177 if l := Location(row, col):
178 if Board.color(l) == color:
179 all_loc.append(l)
180 return all_loc
181
182 def make_move(self, m:Move):
183 piece: PieceBase = self[m.start]
184 self[m.start] = None
185 self[m.stop] = piece
186 if piece.color == Color.BLACK and m.stop.row == 1:
187 self[m.stop] = Queen(Color.BLACK)
188 elif piece.color == Color.WHITE and m.stop.row == self.__size:
189 self[m.stop] = Queen(Color.WHITE)
190 if m.captured is not None:
191 self[m.captured]=None
192
193 @staticmethod
194 def color(loc:Location):
195 if (loc.col + loc.row) % 2 == 0:
196 return Color.BLACK
197 else:
198 return Color.WHITE
199
200 def __str__(self):
201 s = ""
202 for r in range(1, self.__size+1):
203 s += Location.letters[r] + " |"
204 for c in range(1, self.__size + 1):
205 if self.__b[r][c] == None: s += " |"
206 else:
207 s += self.__b[r][c].symbol()+"|"
208 s += "\n"
209 s += " "
210 for c in range(1, self.__size + 1):
211 s += str(c) + "|"
212 return s + "\n"
213
214
215class Player:
216 def __init__(self, color:Color):
217 self.__my_color = color
218
219 @property
220 def color(self): return self.__my_color
221
222 def move(self, board:Board):
223 moves = self.move_proposals(board)
224 if len(moves) == 0: return None
225 # --- check for capturing moves
226 capt = [m for m in moves if m.captured is not None]
227 if len(capt) != 0:
228 return random.choice(capt)
229 else:
230 return random.choice(moves)
231
232 def move_proposals(self, board:Board) -> list[Move]:
233 pcs = board.pieces(self.color)
234 moves = []
235 for loc, p in pcs:
236 moves.extend(p.move_proposals(loc, board))
237 return moves
238
239
240if __name__ == "__main__":
241
242 # random.seed(1)
243 board = Board(8, 12)
244 players = (Player(Color.WHITE), Player(Color.BLACK))
245
246 i = 0
247 while True:
248 the_player = players[i % 2]
249 move = the_player.move(board)
250 if move is None:
251 print("Player", the_player.color, "won", "after", i + 1, "moves")
252 print(board)
253 break
254 else:
255 board.make_move(move)
256 i += 1
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.
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:
self.__viewport.circle("p-"+str(loc), x, y, r/3.0, fill=color, stroke_width="3", stroke="black")
self.__viewport.define_binding("p-"+str(loc), "click", move_start)
def draw_checkerboard(self):
Przy odczytywaniu zaś pobieramy cel (target) zdarzenia (obiekt evt, który jest elementem strony),
jego id zawiera szukany napis:
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:
Po zakończeniu gry wywołania kończy instrukcja: timer.clear_interval(loop_handler) (linia 112). A oto pełen kod gry
interaktywnej:
1from __future__ import annotations
2
3from browser import document, timer
4from checkers2 import *
5from visualife.core import HtmlViewport
6
7
8class CheckersGui:
9
10 def __init__(self, width:float, board:Board, viewport: HtmlViewport):
11 self.__n = Location.board_size
12 self.__side = width/self.__n - 5
13 self.__viewport = viewport
14 self.__board = board
15 self.draw_checkerboard()
16 self.__n_click = 0
17 self.__move_start = None
18 self.__move_end = None
19
20 def draw_piece(self, loc:Location, is_queen):
21
22 # callback to store location of the move starting point: cut the last two characters to handle both "A1" and "p-A1" cases
23 def move_start(evt): self.__move_start = evt.target.id[-2:]
24
25 pc = self.__board[loc]
26 x = (loc.col - 0.5) * self.__side
27 y = (loc.row - 0.5) * self.__side
28 r = self.__side/2 - 3
29 if pc.color == Color.WHITE:
30 color = "white"
31 stroke = "darkgray"
32 self.__viewport.define_binding(str(loc), "click", move_start)
33 else:
34 stroke = "white"
35 color = "darkgray"
36 self.__viewport.circle(str(loc), x, y, r, fill=color, stroke_width="2", stroke=stroke)
37 if is_queen:
38 self.__viewport.circle("p-"+str(loc), x, y, r/3.0, fill=color, stroke_width="3", stroke="black")
39 self.__viewport.define_binding("p-"+str(loc), "click", move_start)
40
41 def draw_checkerboard(self):
42
43 # callback to store location of the move ending location
44 def move_end(evt): self.__move_end = evt.target.id[2:]
45
46 self.__viewport.clear()
47
48 # --- the board itself
49 for loc in self.__board.locations(Color.BLACK):
50 x, y = (loc.col - 1) * self.__side, (loc.row - 1) * self.__side
51 name = "b-" + str(loc) # Every ID of a board square is a location string preceded with "b-"
52 self.__viewport.define_binding(name, "click", move_end)
53 self.__viewport.rect(name, x, y, self.__side, self.__side, fill="black", stroke="black", stroke_width="1")
54
55 for loc, pc in self.__board.pieces(Color.WHITE):
56 self.draw_piece(loc, isinstance(pc, Queen))
57
58 for loc, pc in self.__board.pieces(Color.BLACK):
59 self.draw_piece(loc, isinstance(pc, Queen))
60
61 self.__viewport.close()
62 self.__viewport.apply_binding()
63
64 def get_move(self):
65 if self.__move_start is not None and self.__move_end is not None:
66 out = (self.__move_start, self.__move_end)
67 self.__move_start, self.__move_end = None, None
68 return out # return a move given as the two end points
69 return None # None means no move ready yet!
70
71 def message(self, msg):
72 document["msg"].innerHTML = msg
73
74
75class HumanPlayer(Player):
76 def __init__(self, color:Color, gui:CheckersGui):
77 super().__init__(color)
78 self.gui = gui
79 self.__move_ready = False
80 self.__gui_lock = None
81
82 def move(self, board:Board):
83
84 m = self.gui.get_move()
85 if m is None: return [] # Empty list means move is not yet ready!
86 moves = self.move_proposals(board)
87 # --- check for capturing moves; they are mandatory!
88 capt = [m for m in moves if m.captured is not None]
89
90 move_from, move_to = m # unpack the move since it's not empty
91 for m in moves:
92 if str(m.start) == move_from and str(m.stop) == move_to:
93 if m.captured is None and len(capt) > 0:
94 self.gui.message("you have a capturing move!")
95 return []
96 self.gui.message("") # successful move clears a previous error message
97 return m
98 self.gui.message("illegal move!")
99 return [] # Return "move not ready" signal
100
101
102def play_with_gui(players: list[Player], board: Board, gui: CheckersGui):
103
104 global loop_handler, n_moves
105
106 the_player = players[n_moves % 2]
107 move = the_player.move(board)
108 if move is None:
109 msg = "Player %s won after %d moves!" % (str(Color.enemy_color(the_player.color)), n_moves + 1)
110 gui.message(msg)
111 timer.clear_interval(loop_handler)
112 elif isinstance(move, list) and len(move) == 0: # --- GUI player not ready
113 pass
114 else:
115 print("MOVING: ", move)
116 board.make_move(move)
117 n_moves += 1
118 gui.draw_checkerboard()
119
120
121board_types = {"6x6": (6, 6), "8x8": (8, 12), "10x10": (10, 15)}
122
123def restart(evt=None):
124
125 global loop_handler, drawing
126 board_type = document["select_board"].get(selector='input[name="board"]:checked')[0].id
127 size, n_pieces = board_types[board_type]
128 if loop_handler is not None:
129 timer.clear_interval(loop_handler)
130 drawing.clear()
131 board = Board(size, n_pieces)
132 game = CheckersGui(500, board, drawing)
133 players = (HumanPlayer(Color.WHITE, game), Player(Color.BLACK))
134 loop_handler = timer.set_interval(play_with_gui, 300, players, board, game)
135
136document["restart"].bind("click", restart)
137loop_handler = None
138n_moves = 0
139drawing = HtmlViewport(document['drawing'], 500, 500)
140restart()