13from ipywidgets
import AppLayout, Button, HBox, Layout, Output, VBox, widgets
19 """A class which allows to create an interactive Connect-4 widget.
21 GuiC4 is an interactive Connect-4 graphical user interface (GUI) implemented using
22 Matplotlib, IPython widgets, and a backend agent from the BitBully engine. It
23 provides the following main features:
25 - Interactive Game Board: Presents a dynamic 6-row by 7-column
26 Connect-4 board with clickable board cells.
27 - Matplotlib Integration: Utilizes Matplotlib figures
28 to render high-quality game visuals directly within Jupyter notebook environments.
29 - User Interaction: Captures and processes mouse clicks and button events, enabling
30 intuitive gameplay via either direct board interaction or button controls.
31 - Undo/Redo Moves: Supports undo and redo functionalities, allowing users to
32 navigate through their move history during gameplay.
33 - Automated Agent Moves: Incorporates BitBully, a Connect-4 backend engine, enabling
34 computer-generated moves and board evaluations.
35 - Game State Handling: Detects game-over scenarios, including win/draw conditions,
36 and provides immediate user feedback through popup alerts.
39 notify_output (widgets.Output): Output widget for notifications and popups.
42 Generally, you should this method to retreive and display the widget.
45 >>> %matplotlib ipympl
47 >>> display(c4gui.get_widget())
53 """Init the GuiC4 widget."""
55 self.
m_logger = logging.getLogger(self.__class__.__name__)
56 self.
m_logger.setLevel(logging.DEBUG)
59 ch = logging.StreamHandler()
60 ch.setLevel(logging.INFO)
63 formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s")
64 ch.setFormatter(formatter)
71 assets_pth = Path(str(importlib.resources.files(
"bitbully").joinpath(
"assets")))
72 png_empty = plt.imread(assets_pth.joinpath(
"empty.png"), format=
None)
73 png_empty_m = plt.imread(assets_pth.joinpath(
"empty_m.png"), format=
None)
74 png_empty_r = plt.imread(assets_pth.joinpath(
"empty_r.png"), format=
None)
75 png_red = plt.imread(assets_pth.joinpath(
"red.png"), format=
None)
76 png_red_m = plt.imread(assets_pth.joinpath(
"red_m.png"), format=
None)
77 png_yellow = plt.imread(assets_pth.joinpath(
"yellow.png"), format=
None)
78 png_yellow_m = plt.imread(assets_pth.joinpath(
"yellow_m.png"), format=
None)
80 0: {
"plain": png_empty,
"corner": png_empty_m,
"underline": png_empty_r},
81 1: {
"plain": png_yellow,
"corner": png_yellow_m},
82 2: {
"plain": png_red,
"corner": png_red_m},
88 self.
m_height = np.zeros(7, dtype=np.int32)
109 self.
m_movelist: list[tuple[int, int, int]] = []
112 self.
m_redolist: list[tuple[int, int, int]] = []
121 resource = importlib.resources.files(
"bitbully").joinpath(
"assets/book_12ply_distances.dat")
122 with importlib.resources.as_file(resource)
as db_path:
125 def _reset(self) -> None:
128 self.
m_height = np.zeros(7, dtype=np.int32)
132 im.set_data(self.
m_png[0][
"plain"])
134 self.
m_fig.canvas.draw_idle()
135 self.
m_fig.canvas.flush_events()
138 def _get_fig_size_px(self) -> npt.NDArray[np.float64]:
140 size_in_inches = self.
m_fig.get_size_inches()
141 self.
m_logger.debug(
"Figure size in inches: %f", size_in_inches)
145 self.
m_logger.debug(
"Figure DPI: %d", dpi)
148 return size_in_inches * dpi
150 def _create_control_buttons(self) -> None:
157 wh = f
"{-3 + (fig_size_px[1] / self.m_n_row)}px"
158 btn_layout = Layout(height=wh, width=wh)
160 button = Button(description=
"đ", tooltip=
"Reset Game", layout=btn_layout)
161 button.on_click(
lambda b: self.
_reset())
164 button = Button(description=
"âŠī¸", tooltip=
"Undo Move", layout=btn_layout)
165 button.disabled =
True
169 button = Button(description=
"âĒī¸", tooltip=
"Redo Move", layout=btn_layout)
170 button.disabled =
True
174 button = Button(description=
"đšī¸", tooltip=
"Computer Move", layout=btn_layout)
178 button = Button(description=
"đ", tooltip=
"Evaluate Board", layout=btn_layout)
181 def _computer_move(self) -> None:
184 b = bitbully_core.BoardCore()
185 assert b.setBoard([mv[1]
for mv
in self.
m_movelist])
190 def _create_board(self) -> None:
194 fig, axs = plt.subplots(
205 self.
ims.append(ax.imshow(self.
m_png[0][
"plain"], animated=
True))
207 ax.set_xticklabels([])
208 ax.set_yticklabels([])
211 plt.subplots_adjust(wspace=0.05, hspace=0.05, left=0.0, right=1.0, top=1.0, bottom=0.0)
213 fig.set_facecolor(
"darkgray")
214 fig.canvas.toolbar_visible =
False
215 fig.canvas.resizable =
False
216 fig.canvas.toolbar_visible =
False
217 fig.canvas.header_visible =
False
218 fig.canvas.footer_visible =
False
219 fig.canvas.capture_scroll =
True
220 plt.show(block=
False)
225 notify_output: widgets.Output = widgets.Output()
226 display(notify_output)
228 @notify_output.capture()
229 def _popup(self, text: str) ->
None:
231 display(Javascript(f
"alert('{text}')"))
233 def _is_legal_move(self, col: int) -> bool:
236 def _insert_token(self, col: int, reset_redo_list: bool =
True) ->
None:
242 button.disabled =
True
244 board = bitbully_core.BoardCore()
245 board.setBoard([mv[1]
for mv
in self.
m_movelist])
246 if self.
m_gameover or not board.playMove(col):
265 except Exception
as e:
266 self.
m_logger.error(
"Error: %s", str(e))
274 def _redo_move(self) -> None:
280 def _undo_move(self) -> None:
297 self.
ims[img_idx].set_data(self.
m_png[0][
"plain"])
298 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
302 self.
m_fig.canvas.blit(self.
ims[img_idx].get_clip_box())
303 self.
m_fig.canvas.flush_events()
307 except Exception
as e:
308 self.
m_logger.error(
"Error: %s", str(e))
317 def _update_insert_buttons(self) -> None:
327 """Translates a column and row ID into the corresponding image ID.
330 col (int): column (0-6) of the considered board cell.
331 row (int): row (0-5) of the considered board cell.
334 int: The corresponding image id (0-41).
336 self.
m_logger.debug(
"Got column: %d", col)
340 def _paint_token(self) -> None:
346 self.
m_logger.debug(
"Paint token: %d", img_idx)
351 self.
ims[img_idx].set_data(self.
m_png[p][
"corner"])
356 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
357 blit_boxes.append(self.
ims[img_idx].get_clip_box())
365 self.
ims[img_idx].set_data(self.
m_png[p][
"plain"])
366 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
367 blit_boxes.append(self.
ims[img_idx].get_clip_box())
369 self.
m_fig.canvas.blit(blit_boxes[0])
374 self.
m_fig.canvas.flush_events()
376 def _create_buttons(self) -> None:
383 for col
in range(self.
m_n_col):
386 layout=Layout(width=f
"{-3 + (fig_size_px[0] / self.m_n_col)}px", height=
"50px"),
392 """Creates a row with the column labels 'a' to 'g'.
395 HBox: A row of textboxes containing the columns labels 'a' to 'g'.
398 width = f
"{-3 + (fig_size_px[0] / self.m_n_col)}px"
401 value=chr(ord(
"a") + i),
402 layout=Layout(justify_content=
"center", align_items=
"center", width=width),
410 flex_flow=
"row wrap",
411 justify_content=
"center",
412 align_items=
"center",
417 """Based on the column where the click was detected, insert a token.
420 event (mpl_backend_bases.Event): A matplotlib mouse event.
422 if isinstance(event, mpl_backend_bases.MouseEvent):
423 ix, iy = event.xdata, event.ydata
424 self.
m_logger.debug(
"click (x,y): %d, %d", ix, iy)
425 idx = np.where(self.
m_axs == event.inaxes)[0][0] % self.
m_n_col
432 Generally, you should this method to retreive and display the widget.
435 >>> %matplotlib ipympl
437 >>> display(c4gui.get_widget())
441 AppLayout: the widget.
444 insert_button_row = HBox(
448 flex_flow=
"row wrap",
449 justify_content=
"center",
450 align_items=
"center",
453 control_buttons_col = HBox(
457 flex_flow=
"row wrap",
458 justify_content=
"flex-end",
459 align_items=
"center",
467 left_sidebar=control_buttons_col,
469 [insert_button_row, self.
output, tb],
472 flex_flow=
"column wrap",
473 justify_content=
"flex-start",
474 align_items=
"flex-start",
482 """Check for Win or draw."""
484 winner =
"Yellow" if board.movesLeft() % 2
else "Red"
485 self.
_popup(f
"Game over! {winner} wins!")
487 if board.movesLeft() == 0:
488 self.
_popup(
"Game over! Draw!")
492 """Destroy and release the acquired resources."""
493 plt.close(self.
m_fig)