Skip to content

gui_c4

GUI module for the BitBully Connect-4 interactive widget.

Classes:

Name Description
GuiC4

A class which allows to create an interactive Connect-4 widget.

GuiC4

GuiC4(agents: dict[str, Connect4Agent] | Sequence[Connect4Agent] | None = None, *, autoplay: bool = False)

A class which allows to create an interactive Connect-4 widget.

GuiC4 is an interactive Connect-4 graphical user interface (GUI) implemented using Matplotlib, IPython widgets, and a backend agent from the BitBully engine. It provides the following main features:

  • Interactive Game Board: Presents a dynamic 6-row by 7-column Connect-4 board with clickable board cells.
  • Matplotlib Integration: Utilizes Matplotlib figures to render high-quality game visuals directly within Jupyter notebook environments.
  • User Interaction: Captures and processes mouse clicks and button events, enabling intuitive gameplay via either direct board interaction or button controls.
  • Undo/Redo Moves: Supports undo and redo functionalities, allowing users to navigate through their move history during gameplay.
  • Automated Agent Moves: Incorporates BitBully, a Connect-4 backend engine, enabling computer-generated moves and board evaluations.
  • Game State Handling: Detects game-over scenarios, including win/draw conditions, and provides immediate user feedback through popup alerts.

Attributes:

Name Type Description
notify_output Output

Output widget for notifications and popups.

Examples:

Generally, you should this method to retreive and display the widget.

>>> %matplotlib ipympl
>>> c4gui = GuiC4()
>>> display(c4gui.get_widget())

Methods:

Name Description
destroy

Destroy and release the acquired resources.

get_widget

Get the widget.

Source code in src/bitbully/gui_c4.py
def __init__(
    self,
    agents: dict[str, Connect4Agent] | Sequence[Connect4Agent] | None = None,
    *,
    autoplay: bool = False,
) -> None:
    """Init the GuiC4 widget."""
    # Create a logger with the class name
    self.m_logger = logging.getLogger(self.__class__.__name__)
    self.m_logger.setLevel(logging.DEBUG)  # Set the logging level

    # Create a console handler (optional)
    ch = logging.StreamHandler()
    ch.setLevel(logging.INFO)  # Set level for the handler

    # Create a formatter and add it to the handler
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)

    # Add the handler to the logger
    if not self.m_logger.handlers:
        self.m_logger.addHandler(ch)

    # Avoid adding handlers multiple times
    self.m_logger.propagate = False
    assets_pth = Path(str(importlib.resources.files("bitbully").joinpath("assets")))
    png_empty = plt.imread(assets_pth.joinpath("empty.png"), format=None)
    png_empty_m = plt.imread(assets_pth.joinpath("empty_m.png"), format=None)
    png_empty_r = plt.imread(assets_pth.joinpath("empty_r.png"), format=None)
    png_red = plt.imread(assets_pth.joinpath("red.png"), format=None)
    png_red_m = plt.imread(assets_pth.joinpath("red_m.png"), format=None)
    png_yellow = plt.imread(assets_pth.joinpath("yellow.png"), format=None)
    png_yellow_m = plt.imread(assets_pth.joinpath("yellow_m.png"), format=None)
    self.m_png = {
        0: {"plain": png_empty, "corner": png_empty_m, "underline": png_empty_r},
        1: {"plain": png_yellow, "corner": png_yellow_m},
        2: {"plain": png_red, "corner": png_red_m},
    }

    self.m_n_row, self.m_n_col = 6, 7

    # TODO: probably not needed:
    self.m_height = np.zeros(7, dtype=np.int32)

    self.m_board_size = 3.5
    # self.m_player = 1
    self.is_busy = False

    # ---------------- multi-agent support ----------------
    self.autoplay = bool(autoplay)

    # Normalize `agents` into a dict[str, Connect4Agent]
    if agents is None:
        self.agents: dict[str, Connect4Agent] = {}
    elif isinstance(agents, dict):
        self.agents = dict(agents)
    else:
        self.agents = {f"agent{i + 1}": a for i, a in enumerate(agents)}

    self._agent_names: list[str] = list(self.agents.keys())

    # Which controller plays which color
    # values are either "human" or one of self._agent_names
    self.yellow_player: str = "human"
    self.red_player: str = self._agent_names[0] if self._agent_names else "human"

    # Which agent should be used for the "Evaluate" button.
    # Values: "auto" or one of self._agent_names (if any exist)
    self.eval_agent_choice: str = "auto"

    # Create board first
    self._create_board()

    # timing row (must exist before get_widget())
    self._create_status_bar()

    # Generate buttons for inserting the tokens:
    self._create_buttons()

    # Create control buttons
    self._create_control_buttons()

    # player selection dropdowns (must exist before get_widget())
    self._create_player_selectors()

    # evaluation row widget (must exist before get_widget())
    self._create_eval_row()

    # Capture clicks on the field
    _ = self.m_fig.canvas.mpl_connect("button_press_event", self._on_field_click)

    # Movelist
    self.m_movelist: list[tuple[int, int, int]] = []

    # Redo list
    self.m_redolist: list[tuple[int, int, int]] = []

    # Gameover flag:
    self.m_gameover = False

    # NEW: move list + copy position UI
    self._create_move_list_ui()

agents instance-attribute

agents: dict[str, Connect4Agent] = {}

autoplay instance-attribute

autoplay = bool(autoplay)

eval_agent_choice instance-attribute

eval_agent_choice: str = 'auto'

is_busy instance-attribute

is_busy = False

m_board_size instance-attribute

m_board_size = 3.5

m_gameover instance-attribute

m_gameover = False

m_height instance-attribute

m_height = zeros(7, dtype=int32)

m_logger instance-attribute

m_logger = getLogger(__name__)

m_movelist instance-attribute

m_movelist: list[tuple[int, int, int]] = []

m_png instance-attribute

m_png = {0: {'plain': png_empty, 'corner': png_empty_m, 'underline': png_empty_r}, 1: {'plain': png_yellow, 'corner': png_yellow_m}, 2: {'plain': png_red, 'corner': png_red_m}}

m_redolist instance-attribute

m_redolist: list[tuple[int, int, int]] = []

notify_output class-attribute instance-attribute

notify_output: Output = Output()

red_player instance-attribute

red_player: str = _agent_names[0] if _agent_names else 'human'

yellow_player instance-attribute

yellow_player: str = 'human'

destroy

destroy() -> None

Destroy and release the acquired resources.

Source code in src/bitbully/gui_c4.py
def destroy(self) -> None:
    """Destroy and release the acquired resources."""
    plt.close(self.m_fig)
    del self.agents
    del self.m_axs
    del self.m_fig
    del self.output

get_widget

get_widget() -> AppLayout

Get the widget.

Examples:

Generally, you should this method to retreive and display the widget.

>>> %matplotlib ipympl
>>> c4gui = GuiC4()
>>> display(c4gui.get_widget())

Returns:

Name Type Description
AppLayout AppLayout

the widget.

Source code in src/bitbully/gui_c4.py
def get_widget(self) -> AppLayout:
    """Get the widget.

    Examples:
        Generally, you should this method to retreive and display the widget.

        ```pycon
        >>> %matplotlib ipympl
        >>> c4gui = GuiC4()
        >>> display(c4gui.get_widget())
        ```

    Returns:
        AppLayout: the widget.
    """
    # Arrange buttons in a row
    insert_button_row = HBox(
        [VBox(layout=Layout(padding="0px 0px 0px 6px")), *self.m_insert_buttons],
        layout=Layout(
            display="flex",
            flex_flow="row wrap",  # or "column" depending on your layout needs
            justify_content="center",  # Left alignment
            align_items="center",  # Top alignment
        ),
    )
    control_buttons_col = HBox(
        [VBox(list(self.m_control_buttons.values()))],
        layout=Layout(
            display="flex",
            flex_flow="row wrap",  # or "column" depending on your layout needs
            justify_content="flex-end",
            align_items="center",  # bottom alignment
        ),
    )

    # deactivate for now
    # tb = self._create_column_labels()

    right = VBox(
        [self.move_list_row],
        layout=Layout(
            display="flex",
            flex_flow="column",
            justify_content="flex-start",
            align_items="stretch",
            width="200px",
            height="90%",  # NEW: fill AppLayout height
            flex="1 1 auto",  # NEW: allow it to grow
        ),
    )

    main = HBox(
        [
            VBox(
                [
                    self.player_select_row,
                    insert_button_row,
                    self.output,
                    self.m_eval_row,
                    self.m_time_row,
                ],
                layout=Layout(
                    display="flex",
                    flex_flow="column",
                    align_items="flex-start",
                ),
            ),
            right,
        ],
        layout=Layout(
            display="flex",
            flex_flow="row",
            align_items="flex-start",
            justify_content="flex-start",
            gap="5px",  # space between board and sidebar
            width="100%",
        ),
    )

    return AppLayout(
        header=None,
        left_sidebar=control_buttons_col,
        center=main,
        right_sidebar=None,  # <= important
        footer=None,
        layout=Layout(grid_gap="0px"),
    )