BitBully 0.0.39
Loading...
Searching...
No Matches
gui_c4.py
1import matplotlib.pyplot as plt
2import numpy as np
3import time
4import logging
5from ipywidgets import widgets
6from IPython.display import Javascript, display, clear_output
7from ipywidgets import Button, VBox, HBox, Output, Layout
8from ipywidgets import AppLayout
9from bitbully import bitbully_core
10import importlib.resources
11
12
13class GuiC4:
14 def __init__(self):
15 # Create a logger with the class name
16 self.m_logger = logging.getLogger(self.__class__.__name__)
17 self.m_logger.setLevel(logging.DEBUG) # Set the logging level
18
19 # Create a console handler (optional)
20 ch = logging.StreamHandler()
21 ch.setLevel(logging.INFO) # Set level for the handler
22
23 # Create a formatter and add it to the handler
24 formatter = logging.Formatter(
25 "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
26 )
27 ch.setFormatter(formatter)
28
29 # Add the handler to the logger
30 self.m_logger.addHandler(ch)
31
32 # Avoid adding handlers multiple times
33 self.m_logger.propagate = False
34 assets_pth = importlib.resources.files("bitbully").joinpath("assets")
35 with assets_pth.joinpath("empty.png").open("rb") as file:
36 png_empty = plt.imread(file, format=None)
37 with assets_pth.joinpath("empty_m.png").open("rb") as file:
38 png_empty_m = plt.imread(file, format=None)
39 with assets_pth.joinpath("empty_r.png").open("rb") as file:
40 png_empty_r = plt.imread(file, format=None)
41 with assets_pth.joinpath("red.png").open("rb") as file:
42 png_red = plt.imread(file, format=None)
43 with assets_pth.joinpath("red_m.png").open("rb") as file:
44 png_red_m = plt.imread(file, format=None)
45 with assets_pth.joinpath("yellow.png").open("rb") as file:
46 png_yellow = plt.imread(file, format=None)
47 with assets_pth.joinpath("yellow_m.png").open("rb") as file:
48 png_yellow_m = plt.imread(file, format=None)
49 self.m_png = {
50 0: {"plain": png_empty, "corner": png_empty_m, "underline": png_empty_r},
51 1: {"plain": png_yellow, "corner": png_yellow_m},
52 2: {"plain": png_red, "corner": png_red_m},
53 }
54
55 self.m_n_row, self.m_n_col = 6, 7
56
57 # TODO: probably not needed:
58 self.m_height = np.zeros(7, dtype=np.int32)
59
60 self.m_board_size = 3.5
61 # self.m_player = 1
62 self.is_busy = False
63
64 self.last_event_time = time.time()
65
66 # Create board first
67 self.create_board()
68
69 # Generate buttons for inserting the tokens:
70 self.create_buttons()
71
72 # Create control buttons
74
75 # Capture clicks on the field
76 _ = self.m_fig.canvas.mpl_connect("button_press_event", self.on_field_click)
77
78 # Movelist
79 self.m_movelist = []
80
81 # Redo list
82 self.m_redolist = []
83
84 # Gameover flag:
85 self.m_gameover = False
86
87 # C4 agent
88 db_path = importlib.resources.files("bitbully").joinpath(
89 "assets/book_12ply_distances.dat"
90 )
91 self.bitbully_agent = bitbully_core.BitBully(db_path)
92
93 def reset(self):
94 self.m_movelist = []
95 self.m_redolist = []
96 self.m_height = np.zeros(7, dtype=np.int32)
97 self.m_gameover = False
98
99 for im in self.ims:
100 im.set_data(self.m_png[0]["plain"])
101
102 self.m_fig.canvas.draw_idle()
103 self.m_fig.canvas.flush_events()
105
106 def get_fig_size_px(self):
107 # Get the size in inches
108 size_in_inches = self.m_fig.get_size_inches()
109 self.m_logger.debug(f"Figure size in inches: {size_in_inches}")
110
111 # Get the DPI
112 dpi = self.m_fig.dpi
113 self.m_logger.debug(f"Figure DPI: {dpi}")
114
115 # Convert to pixels
116 fig_size_in_pixels = size_in_inches * dpi
117
118 # Alternatively:
119 # self.m_logger.debug(f"Figure size in pixels: {fig_size_in_pixels}")
120 # bbox = fig.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
121 # width, height = bbox.width*fig.dpi, bbox.height*fig.dpi
122 # self.m_logger.debug(width, height)
123 # return tuple(round(x) for x in fig_size_in_pixels)
124 return fig_size_in_pixels
125
126 def create_control_buttons(self):
127 self.m_control_buttons = {}
128
129 # Create buttons for each column
130 self.m_logger.debug("Figure size: ", self.get_fig_size_px())
131
132 fig_size_px = self.get_fig_size_px()
133 wh = f"{-3 + (fig_size_px[1] / self.m_n_row)}px"
134 btn_layout = Layout(height=wh, width=wh)
135
136 button = Button(description="🔄", tooltip="Reset Game", layout=btn_layout)
137 button.on_click(lambda b: self.reset())
138 self.m_control_buttons["reset"] = button
139
140 button = Button(description="â†Šī¸", tooltip="Undo Move", layout=btn_layout)
141 button.disabled = True
142 button.on_click(lambda b: self.undo_move())
143 self.m_control_buttons["undo"] = button
144
145 button = Button(description="â†Ēī¸", tooltip="Redo Move", layout=btn_layout)
146 button.disabled = True
147 button.on_click(lambda b: self.redo_move())
148 self.m_control_buttons["redo"] = button
149
150 button = Button(description="đŸ•šī¸", tooltip="Computer Move", layout=btn_layout)
151 button.on_click(lambda b: self.computer_move())
152 self.m_control_buttons["move"] = button
153
154 button = Button(description="📊", tooltip="Evaluate Board", layout=btn_layout)
155 self.m_control_buttons["evaluate"] = button
156
157 def computer_move(self):
158 self.is_busy = True
160 b = bitbully_core.Board()
161 assert b.setBoard([mv[1] for mv in self.m_movelist])
162 move_scores = self.bitbully_agent.scoreMoves(b)
163 self.is_busy = False
164 self.insert_token(np.argmax(move_scores))
165
166 def create_board(self):
167 self.output = Output()
168
169 with self.output:
170 fig, axs = plt.subplots(
171 self.m_n_row,
172 self.m_n_col,
173 figsize=(
174 self.m_board_size / self.m_n_row * self.m_n_col,
175 self.m_board_size,
176 ),
177 )
178 axs = axs.flatten()
179 self.ims = list()
180 for ax in axs:
181 self.ims.append(ax.imshow(self.m_png[0]["plain"], animated=True))
182 ax.axis("off")
183 ax.set_xticklabels([])
184 ax.set_yticklabels([])
185
186 fig.tight_layout()
187 plt.subplots_adjust(
188 wspace=0.05, hspace=0.05, left=0.0, right=1.0, top=1.0, bottom=0.0
189 )
190 fig.suptitle("")
191 fig.canvas.toolbar_visible = False
192 fig.canvas.resizable = False
193 fig.set_facecolor("darkgray")
194 fig.canvas.toolbar_visible = False
195 fig.canvas.header_visible = False
196 fig.canvas.footer_visible = False
197 fig.canvas.capture_scroll = True
198 plt.show(block=False)
199
200 self.m_fig = fig
201 self.m_axs = axs
202
203 # bacground does not appear to be necessary here
204 # self.m_background = [fig.canvas.copy_from_bbox(im.get_clip_box()) for im in self.ims]
205 # for b in self.m_background:
206 # fig.canvas.blit(b)
207
208 notify_output = widgets.Output()
209 display(notify_output)
210
211 @notify_output.capture()
212 def popup(self, text):
213 clear_output()
214 display(Javascript("alert('{}')".format(text)))
215
216 def is_legal_move(self, col):
217 if self.m_height[col] >= self.m_n_row:
218 return False
219 return True
220
221 def insert_token(self, col, reset_redo_list=True):
222 if self.is_busy:
223 return
224 self.is_busy = True
225
226 for button in self.m_insert_buttons:
227 button.disabled = True
228
229 board = bitbully_core.Board()
230 board.setBoard([mv[1] for mv in self.m_movelist])
231 if self.m_gameover or not board.playMove(col):
233 self.is_busy = False
234 return
235
236 try:
237 # Get player
238 player = 1 if not self.m_movelist else 3 - self.m_movelist[-1][0]
239 self.m_movelist.append((player, col, self.m_height[col]))
240 self.paint_token()
241 self.m_height[col] += 1
242
243 # Usually, after a move is performed, there is no possibility to
244 # redo a move again
245 if reset_redo_list:
246 self.m_redolist = []
247
248 self.check_winner(board)
249
250 except Exception as e:
251 self.m_logger.error(f"Error: {e}")
252 raise
253 finally:
254 time.sleep(0.5) # debounce button
255 # Re-enable all buttons (if columns not full)
256 self.is_busy = False
258
259 def redo_move(self):
260 if len(self.m_redolist) < 1:
261 return
262 p, col, row = self.m_redolist.pop()
263 self.insert_token(col, reset_redo_list=False)
264
265 def undo_move(self):
266 if len(self.m_movelist) < 1:
267 return
268
269 if self.is_busy:
270 return
271 self.is_busy = True
272
273 try:
274 p, col, row = mv = self.m_movelist.pop()
275 self.m_redolist.append(mv)
276
277 self.m_height[col] -= 1
278 assert row == self.m_height[col]
279
280 img_idx = self.get_img_idx(col, row)
281
282 self.ims[img_idx].set_data(self.m_png[0]["plain"])
283 self.m_axs[img_idx].draw_artist(self.ims[img_idx])
284 if len(self.m_movelist) > 0:
285 self.paint_token()
286 else:
287 self.m_fig.canvas.blit(self.ims[img_idx].get_clip_box())
288 self.m_fig.canvas.flush_events()
289
290 self.m_gameover = False
291
292 except Exception as e:
293 self.m_logger.error(f"Error: {e}")
294 raise
295 finally:
296 # Re-enable all buttons (if columns not full)
297 self.is_busy = False
299
300 time.sleep(0.5) # debounce button
301
302 def update_insert_buttons(self):
303 for button, col in zip(self.m_insert_buttons, range(self.m_n_col)):
304 button.disabled = (
305 bool(self.m_height[col] >= self.m_n_row)
306 or self.m_gameover
307 or self.is_busy
308 )
309
310 self.m_control_buttons["undo"].disabled = (
311 len(self.m_movelist) < 1 or self.is_busy
312 )
313 self.m_control_buttons["redo"].disabled = (
314 len(self.m_redolist) < 1 or self.is_busy
315 )
316 self.m_control_buttons["move"].disabled = self.m_gameover or self.is_busy
317 self.m_control_buttons["evaluate"].disabled = self.m_gameover or self.is_busy
318
319 def get_img_idx(self, col, row):
320 """
321 Get the index of the image to paint.
322
323 This corresponds to the last token in the column
324 """
325 self.m_logger.debug(f"Got column: {col}")
326
327 img_idx = col % self.m_n_col + (self.m_n_row - row - 1) * self.m_n_col
328 self.m_logger.debug(f"{col}, {img_idx}")
329 return img_idx
330
331 def paint_token(self):
332 if len(self.m_movelist) < 1:
333 return
334
335 p, col, row = self.m_movelist[-1]
336 img_idx = self.get_img_idx(col, row)
337 self.m_logger.debug(f"Paint token: {img_idx}")
338
339 #
340 # no need to reset background, since we anyhow overwrite it again
341 # self.m_fig.canvas.restore_region(self.m_background[img_idx])
342 self.ims[img_idx].set_data(self.m_png[p]["corner"])
343
344 # see: https://matplotlib.org/3.4.3/Matplotlib.pdf
345 # 2.3.1 Faster rendering by using blitting
346 blit_boxes = []
347 self.m_axs[img_idx].draw_artist(self.ims[img_idx])
348 blit_boxes.append(self.ims[img_idx].get_clip_box())
349 # self.m_fig.canvas.blit()
350
351 if len(self.m_movelist) > 1:
352 # Remove the white corners for the second-to-last move
353 # TODO: redundant code above
354 p, col, row = self.m_movelist[-2]
355 img_idx = self.get_img_idx(col, row)
356 self.ims[img_idx].set_data(self.m_png[p]["plain"])
357 self.m_axs[img_idx].draw_artist(self.ims[img_idx])
358 blit_boxes.append(self.ims[img_idx].get_clip_box())
359
360 self.m_fig.canvas.blit(blit_boxes[0])
361
362 # self.m_fig.canvas.restore_region(self.m_background[img_idx])
363 # self.m_fig.canvas.blit(self.ims[img_idx].get_clip_box())
364 # self.m_fig.canvas.draw_idle()
365 self.m_fig.canvas.flush_events()
366
367 def create_buttons(self):
368 # Create buttons for each column
369 self.m_logger.debug("Figure size: ", self.get_fig_size_px())
370
371 fig_size_px = self.get_fig_size_px()
372
373 self.m_insert_buttons = []
374 for col in range(self.m_n_col):
375 button = Button(
376 description="âŦ",
377 layout=Layout(
378 width=f"{-3 + (fig_size_px[0] / self.m_n_col)}px", height="50px"
379 ),
380 )
381 button.on_click(lambda b, col=col: self.insert_token(col))
382 self.m_insert_buttons.append(button)
383
384 def create_column_labels(self):
385 # col_labels = []
386
387 fig_size_px = self.get_fig_size_px()
388 width = f"{-3 + (fig_size_px[0] / self.m_n_col)}px"
389 # textboxes = [Text("a", layout=Layout(width="55px", align="center")) for i in range(7)]
390 textboxes = [
391 widgets.Label(
392 value=chr(ord("a") + i),
393 layout=Layout(
394 justify_content="center", align_items="center", width=width
395 ),
396 )
397 for i in range(self.m_n_col)
398 ]
399 tb = HBox(
400 textboxes,
401 layout=Layout(
402 display="flex",
403 flex_flow="row wrap", # or "column" depending on your layout needs
404 justify_content="center", # Left alignment
405 align_items="center", # Top alignment
406 ),
407 )
408 return tb
409
410 def on_field_click(self, event):
411 ix, iy = event.xdata, event.ydata
412 self.m_logger.debug(f"click (x,y): {ix, iy}")
413 idx = np.where(self.m_axs == event.inaxes)[0][0] % self.m_n_col
414 # if self.is_legal_move(idx):
415 self.insert_token(idx)
416
417 def get_widget(self):
418 # Arrange buttons in a row
419 insert_button_row = HBox(
420 self.m_insert_buttons,
421 layout=Layout(
422 display="flex",
423 flex_flow="row wrap", # or "column" depending on your layout needs
424 justify_content="center", # Left alignment
425 align_items="center", # Top alignment
426 ),
427 )
428 control_buttons_col = HBox(
429 [VBox(list(self.m_control_buttons.values()))],
430 layout=Layout(
431 display="flex",
432 flex_flow="row wrap", # or "column" depending on your layout needs
433 justify_content="flex-end", # Left alignment
434 align_items="center", # Top alignment
435 ),
436 )
437
438 tb = self.create_column_labels()
439
440 return AppLayout(
441 header=None,
442 left_sidebar=control_buttons_col,
443 center=VBox(
444 [insert_button_row, self.output, tb],
445 layout=Layout(
446 display="flex",
447 flex_flow="column wrap", # or "column" depending on your layout needs
448 justify_content="flex-start", # Left alignment
449 align_items="flex-start", # Top alignment
450 ),
451 ),
452 footer=None,
453 right_sidebar=None,
454 )
455
456 def check_winner(self, board):
457 """
458 Check for Win or draw.
459 """
460 if board.hasWin():
461 winner = "Yellow" if board.movesLeft() % 2 else "Red"
462 self.popup(f"Game over! {winner} wins!")
463 self.m_gameover = True
464 if board.movesLeft() == 0:
465 self.popup("Game over! Draw!")
466 self.m_gameover = True
467
468 def destroy(self):
469 plt.close(self.m_fig)
create_control_buttons(self)
Definition gui_c4.py:126
on_field_click(self, event)
Definition gui_c4.py:410
check_winner(self, board)
Definition gui_c4.py:456
popup(self, text)
Definition gui_c4.py:212
get_img_idx(self, col, row)
Definition gui_c4.py:319
insert_token(self, col, reset_redo_list=True)
Definition gui_c4.py:221
update_insert_buttons(self)
Definition gui_c4.py:302
create_column_labels(self)
Definition gui_c4.py:384