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]] = []
118 import bitbully_databases
as bbd
121 db_path: str = bbd.BitBullyDatabases.get_database_path(
"12-ply-dist")
124 def _reset(self) -> None:
127 self.
m_height = np.zeros(7, dtype=np.int32)
131 im.set_data(self.
m_png[0][
"plain"])
133 self.
m_fig.canvas.draw_idle()
134 self.
m_fig.canvas.flush_events()
137 def _get_fig_size_px(self) -> npt.NDArray[np.float64]:
139 size_in_inches = self.
m_fig.get_size_inches()
140 self.
m_logger.debug(
"Figure size in inches: %f", size_in_inches)
144 self.
m_logger.debug(
"Figure DPI: %d", dpi)
147 return size_in_inches * dpi
149 def _create_control_buttons(self) -> None:
156 wh = f
"{-3 + (fig_size_px[1] / self.m_n_row)}px"
157 btn_layout = Layout(height=wh, width=wh)
159 button = Button(description=
"đ", tooltip=
"Reset Game", layout=btn_layout)
160 button.on_click(
lambda b: self.
_reset())
163 button = Button(description=
"âŠī¸", tooltip=
"Undo Move", layout=btn_layout)
164 button.disabled =
True
168 button = Button(description=
"âĒī¸", tooltip=
"Redo Move", layout=btn_layout)
169 button.disabled =
True
173 button = Button(description=
"đšī¸", tooltip=
"Computer Move", layout=btn_layout)
177 button = Button(description=
"đ", tooltip=
"Evaluate Board", layout=btn_layout)
180 def _computer_move(self) -> None:
183 b = bitbully_core.BoardCore()
184 assert b.setBoard([mv[1]
for mv
in self.
m_movelist])
189 def _create_board(self) -> None:
193 fig, axs = plt.subplots(
204 self.
ims.append(ax.imshow(self.
m_png[0][
"plain"], animated=
True))
206 ax.set_xticklabels([])
207 ax.set_yticklabels([])
210 plt.subplots_adjust(wspace=0.05, hspace=0.05, left=0.0, right=1.0, top=1.0, bottom=0.0)
212 fig.set_facecolor(
"darkgray")
213 fig.canvas.toolbar_visible =
False
214 fig.canvas.resizable =
False
215 fig.canvas.toolbar_visible =
False
216 fig.canvas.header_visible =
False
217 fig.canvas.footer_visible =
False
218 fig.canvas.capture_scroll =
True
219 plt.show(block=
False)
224 notify_output: widgets.Output = widgets.Output()
225 display(notify_output)
227 @notify_output.capture()
228 def _popup(self, text: str) ->
None:
230 display(Javascript(f
"alert('{text}')"))
232 def _is_legal_move(self, col: int) -> bool:
235 def _insert_token(self, col: int, reset_redo_list: bool =
True) ->
None:
241 button.disabled =
True
243 board = bitbully_core.BoardCore()
244 board.setBoard([mv[1]
for mv
in self.
m_movelist])
264 except Exception
as e:
265 self.
m_logger.error(
"Error: %s", str(e))
273 def _redo_move(self) -> None:
279 def _undo_move(self) -> None:
296 self.
ims[img_idx].set_data(self.
m_png[0][
"plain"])
297 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
301 self.
m_fig.canvas.blit(self.
ims[img_idx].get_clip_box())
302 self.
m_fig.canvas.flush_events()
306 except Exception
as e:
307 self.
m_logger.error(
"Error: %s", str(e))
316 def _update_insert_buttons(self) -> None:
326 """Translates a column and row ID into the corresponding image ID.
329 col (int): column (0-6) of the considered board cell.
330 row (int): row (0-5) of the considered board cell.
333 int: The corresponding image id (0-41).
335 self.
m_logger.debug(
"Got column: %d", col)
339 def _paint_token(self) -> None:
345 self.
m_logger.debug(
"Paint token: %d", img_idx)
350 self.
ims[img_idx].set_data(self.
m_png[p][
"corner"])
355 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
356 blit_boxes.append(self.
ims[img_idx].get_clip_box())
364 self.
ims[img_idx].set_data(self.
m_png[p][
"plain"])
365 self.
m_axs[img_idx].draw_artist(self.
ims[img_idx])
366 blit_boxes.append(self.
ims[img_idx].get_clip_box())
368 self.
m_fig.canvas.blit(blit_boxes[0])
373 self.
m_fig.canvas.flush_events()
375 def _create_buttons(self) -> None:
382 for col
in range(self.
m_n_col):
385 layout=Layout(width=f
"{-3 + (fig_size_px[0] / self.m_n_col)}px", height=
"50px"),
391 """Creates a row with the column labels 'a' to 'g'.
394 HBox: A row of textboxes containing the columns labels 'a' to 'g'.
397 width = f
"{-3 + (fig_size_px[0] / self.m_n_col)}px"
400 value=chr(ord(
"a") + i),
401 layout=Layout(justify_content=
"center", align_items=
"center", width=width),
409 flex_flow=
"row wrap",
410 justify_content=
"center",
411 align_items=
"center",
416 """Based on the column where the click was detected, insert a token.
419 event (mpl_backend_bases.Event): A matplotlib mouse event.
421 if isinstance(event, mpl_backend_bases.MouseEvent):
422 ix, iy = event.xdata, event.ydata
423 self.
m_logger.debug(
"click (x,y): %d, %d", ix, iy)
424 idx = np.where(self.
m_axs == event.inaxes)[0][0] % self.
m_n_col
431 Generally, you should this method to retreive and display the widget.
434 >>> %matplotlib ipympl
436 >>> display(c4gui.get_widget())
440 AppLayout: the widget.
443 insert_button_row = HBox(
447 flex_flow=
"row wrap",
448 justify_content=
"center",
449 align_items=
"center",
452 control_buttons_col = HBox(
456 flex_flow=
"row wrap",
457 justify_content=
"flex-end",
458 align_items=
"center",
466 left_sidebar=control_buttons_col,
468 [insert_button_row, self.
output, tb],
471 flex_flow=
"column wrap",
472 justify_content=
"flex-start",
473 align_items=
"flex-start",
481 """Check for Win or draw."""
483 winner =
"Yellow" if board.movesLeft() % 2
else "Red"
484 self.
_popup(f
"Game over! {winner} wins!")
486 if board.movesLeft() == 0:
487 self.
_popup(
"Game over! Draw!")
491 """Destroy and release the acquired resources."""
492 plt.close(self.
m_fig)