示例#1
0
 def plot_init(self, ax: plt.Axes):
     self.artists = []
     ax.clear()
     _apply_hook(ax, self.hook)
     for i in range(self.data.shape[0]):
         (liner,) = ax.plot([], [], lw=2, label=str(i))
         self.artists.append(liner)
示例#2
0
def graph_force_disp(hist: history.DB, db_res_case, ax: plt.Axes):

    ax.clear()

    fd_data = list(hist.get_all(history.LoadDisplacementPoint))

    # Put marks on the final increment of each load step
    res_case_data = list(hist.get_all(history.ResultCase))
    res_case_data.sort()
    maj_to_max_res_case = dict()
    for res_case in res_case_data:
        maj_to_max_res_case[res_case.major_inc] = res_case.num

    final_minor_inc_cases = set(maj_to_max_res_case.values())

    def sort_key(ld_point: history.LoadDisplacementPoint):
        return ld_point.node_num, ld_point.load_text, ld_point.disp_text

    fd_data.sort(key=sort_key)

    for (node_num, load_text,
         disp_text), fd_points in itertools.groupby(fd_data, sort_key):
        fd_points = list(fd_points)
        x = [p.disp_val for p in fd_points]
        y = [p.load_val for p in fd_points]

        show_marker = [
            p.result_case_num in final_minor_inc_cases for p in fd_points
        ]

        ax.plot(x,
                y,
                marker='x',
                markevery=show_marker,
                label=f"{node_num} {disp_text} vs {load_text}")

        # Hacky getting DoF
        dof_iter = (dof for dof in st7.DoF if dof.rx_mz_text == load_text)
        dof = next(dof_iter)

        ax.set_xlabel(dof.disp_or_rotation_text)
        ax.set_ylabel(dof.force_or_moment_text)

        # Point to show current db res case
        x_one = [
            p.disp_val for p in fd_points if p.result_case_num == db_res_case
        ]
        y_one = [
            p.load_val for p in fd_points if p.result_case_num == db_res_case
        ]

        ax.plot(x_one,
                y_one,
                marker='o',
                markeredgecolor='tab:orange',
                markerfacecolor='tab:orange')

    ax.legend()
示例#3
0
def clear_axis(ax: plt.Axes):
    """Clear all plotted objects on an axis including lines, patches, tests, tables,
    artists, images, mouseovers, child axes, legends, collections, and containers.
    See: https://matplotlib.org/devdocs/api/_as_gen/matplotlib.axes.Axes.clear.html

    Args:
        ax (plt.Axes): MPL axis to clear
    """
    ax.clear()
示例#4
0
    def plot_init(self, ax: plt.Axes):
        """Copied from https://matplotlib.org/3.1.1/gallery/animation/animated_histogram.html"""
        import matplotlib.patches as patches
        import matplotlib.path as path

        self.artists = []
        ax.clear()
        _apply_hook(ax, self.hook)
        init_data = self.data.flatten()
        n, bins = np.histogram(init_data, self.num_bins)

        # get the corners of the rectangles for the histogram
        left = np.array(bins[:-1])
        right = np.array(bins[1:])
        self._bottom = np.zeros(len(left))
        top = self._bottom + n / (self.num_frames * 0.9)
        nrects = len(left)

        # here comes the tricky part -- we have to set up the vertex and path
        # codes arrays using moveto, lineto and closepoly

        # for each rect: 1 for the MOVETO, 3 for the LINETO, 1 for the
        # CLOSEPOLY; the vert for the closepoly is ignored but we still need
        # it to keep the codes aligned with the vertices
        nverts = nrects * (1 + 3 + 1)
        self._verts = np.zeros((nverts, 2))
        codes = np.ones(nverts, int) * path.Path.LINETO
        codes[0::5] = path.Path.MOVETO
        codes[4::5] = path.Path.CLOSEPOLY

        self._verts[0::5, 0] = left
        self._verts[1::5, 0] = left

        self._verts[2::5, 0] = right
        self._verts[3::5, 0] = right

        self._verts[1::5, 1] = top
        self._verts[2::5, 1] = top

        self._verts[0::5, 1] = self._bottom
        self._verts[3::5, 1] = self._bottom

        barpath = path.Path(self._verts, codes)
        patch = patches.PathPatch(barpath, facecolor="green", edgecolor="yellow", alpha=0.5)
        ax.add_patch(patch)

        ax.set_xlim(left[0], right[-1])
        ax.set_ylim(self._bottom.min(), top.max())
        self.artists.append(patch)
示例#5
0
def graph_surface_profile(deviation_only: bool,
                          top_nodes: typing.FrozenSet[int], hist: history.DB,
                          db_res_case: int, ax: plt.Axes):

    ax.clear()

    for line_data in _get_graph_surface_profile_data(deviation_only, top_nodes,
                                                     hist, db_res_case):
        ax.plot(line_data.x_data,
                line_data.y_data,
                color=line_data.colour,
                label=line_data.label,
                linestyle=line_data.linestyle)

    ax.legend()
示例#6
0
def graph_frame(hist: history.DB, db_res_case: int,
                contour_key: history.ContourKey, ax: plt.Axes):
    row_skeleton = history.ColumnResult._all_nones()._replace(
        result_case_num=db_res_case)
    column_data = list(hist.get_all_matching(row_skeleton))

    col_data_graph = [
        cd for cd in column_data if cd.contour_key == contour_key
    ]

    ax.clear()
    graphed_something = False

    for yielded, base_col in (
        (False, 'tab:blue'),
        (True, 'tab:orange'),
    ):
        # Fall back to NaNs, only override with the real data.
        x_to_cd = {
            cd.x: history.ColumnResult._nans_at(cd.x)
            for cd in col_data_graph
        }

        # Override with the real thing
        for cd in col_data_graph:
            if cd.yielded == yielded:
                x_to_cd[cd.x] = cd
                graphed_something = True

        res_to_plot = [cd for x, cd in sorted(x_to_cd.items())]
        x = [cd.x for cd in res_to_plot]
        y_min = [cd.minimum for cd in res_to_plot]
        y_mean = [cd.mean for cd in res_to_plot]
        y_max = [cd.maximum for cd in res_to_plot]

        ax.fill_between(x, y_min, y_max, color=base_col, alpha=0.2)
        yield_text = "Dilated" if yielded else "Undilated"
        ax.plot(x,
                y_mean,
                color=base_col,
                label=f"{contour_key.name} ({yield_text})")

    ax.legend()

    return graphed_something
示例#7
0
def plot_board(ax_board: plt.Axes, board: Board, nums: IntPair,
               num_offsets: IntPair, index: int):
    ax_board.clear()
    ax_board.set_title(str(index))
    for num, offset, top_y in zip(nums, num_offsets, PlotConst.col_top_y):
        # for loop by raw

        col_top_x = np.linspace(0, 1, num + 2)[1:-1]
        for cc in range(num):
            # flor loop by column

            column = board.get_board()[cc + offset]
            for ee in range(Column.LEN):
                ax_board.plot([col_top_x[cc]],
                              [top_y - ee * PlotConst.y_pitch],
                              color=column[ee].to_color_string(),
                              **PlotConst.plot_args)
    ax_board.set_xlim([0, 1.1])
    ax_board.set_ylim([0, 1.1])
    ax_board.axis("off")
示例#8
0
    def process_frame(img, processing_config: DotMap, color_ranges: dict,
                      do_canny: bool, ax: plt.Axes) -> (plt.Axes, QImage):
        """Main function which takes the camera bgr_frame (bgr_frame since opencv) and
        processes it such that the resulting image (QImage format) can be displayed
        next to the input image."""
        ax.clear()
        if do_canny:
            rgb_img = conversions.bgr2rgb(img)
            gray_img = cv2.cvtColor(rgb_img, cv2.COLOR_RGB2GRAY)
            edged = cv2.Canny(gray_img, 50, 100)
            qt_img_processed = conversions.gray2qt(edged)

        else:
            ax, qt_img_processed = planvec.pipeline.run_pipeline(
                img.copy(),
                ax=ax,
                config=processing_config,
                color_ranges=color_ranges,
                verbose=False,
                visualize_steps=False)
        return ax, qt_img_processed
示例#9
0
class TicTacToeGui(QMainWindow):
    r"""The Tic Tac Toe GUI."""
    def __init__(self,
                 filename=None,
                 board=None,
                 cur_move=None,
                 cur_game=None,
                 game_hist=None):
        # call super method
        super().__init__()
        # initialize the state data
        self.initialize_state(filename, board, cur_move, cur_game, game_hist)
        # call init method to instantiate the GUI
        self.init()

    #%% State initialization
    def initialize_state(self, filename, board, cur_move, cur_game,
                         game_hist):  # TODO: use these other arguments
        r"""Loads the previous game based on settings and whether the file exists."""
        # preallocate to not load
        load_game = False
        self.load_widget = None
        if OPTS.load_previous_game == 'No':
            pass
        elif OPTS.load_previous_game == 'Yes':
            load_game = True
        # ask if loading
        elif OPTS.load_previous_game == 'Ask':
            self.load_widget = QWidget()
            reply = QMessageBox.question(self.load_widget, 'Message', \
                "Do you want to load the previous game?", QMessageBox.Yes | \
                QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                load_game = True
        else:
            raise ValueError(
                'Unexpected value for the load_previous_game option.')
        # initialize outputs
        self.state = State()
        # load previous game
        if load_game:
            if filename is None:
                filename = os.path.join(get_output_dir(), 'tictactoe.pkl')
            if os.path.isfile(filename):
                self.state.game_hist = GameStats.load(filename)
                self.state.cur_game = Counter(len(self.state.game_hist) - 1)
                self.state.cur_move = Counter(
                    len(self.state.game_hist[-1].move_list))
                self.state.board       = create_board_from_moves(self.state.game_hist[-1].move_list, \
                    self.state.game_hist[-1].first_move)
            else:
                raise ValueError(
                    f'Could not find file: "{filename}"')  # pragma: no cover

    #%% GUI initialization
    def init(self):
        r"""Initializes the GUI."""
        #%% properties
        QToolTip.setFont(QtGui.QFont('SansSerif', 10))

        # Central Widget
        self.gui_widget = QWidget(self)
        self.setCentralWidget(self.gui_widget)

        # Panels
        self.grp_score = QWidget()
        self.grp_move = QWidget()
        self.grp_buttons = QWidget()
        self.grp_board = QWidget()
        self.grp_main = QWidget()

        #%% Layouts
        layout_gui = QVBoxLayout(self.gui_widget)
        layout_main = QHBoxLayout(self.grp_main)
        layout_board = QVBoxLayout(self.grp_board)
        layout_buttons = QHBoxLayout(self.grp_buttons)
        layout_score = QGridLayout(self.grp_score)
        layout_move = QVBoxLayout(self.grp_move)

        for layout in [
                layout_gui, layout_main, layout_board, layout_buttons,
                layout_score, layout_move
        ]:
            layout.setAlignment(QtCore.Qt.AlignCenter)

        #%% Text
        # Tic Tac Toe
        lbl_tictactoe = QLabel('Tic Tac Toe')
        lbl_tictactoe.setStyleSheet('font-size: 18pt; font: bold;')
        lbl_tictactoe.setMinimumWidth(220)
        # Score
        lbl_score = QLabel('Score:')
        lbl_score.setStyleSheet('font-size: 12pt; font: bold;')
        lbl_score.setMinimumWidth(220)
        # Move
        lbl_move = QLabel('Move:')
        lbl_move.setStyleSheet('font-size: 12pt; font: bold;')
        lbl_move.setMinimumWidth(220)
        # O Wins
        lbl_o = QLabel('O Wins:')
        lbl_o.setMinimumWidth(80)
        # X Wins
        lbl_x = QLabel('X Wins:')
        lbl_x.setMinimumWidth(80)
        # Games Tied
        lbl_games = QLabel('Games Tied:')
        lbl_games.setMinimumWidth(80)

        for label in [
                lbl_tictactoe, lbl_score, lbl_move, lbl_o, lbl_x, lbl_games
        ]:
            label.setAlignment(QtCore.Qt.AlignCenter)

        # Changeable labels
        self.lbl_o_wins = QLabel('0')
        self.lbl_x_wins = QLabel('0')
        self.lbl_games_tied = QLabel('0')

        for label in [self.lbl_o_wins, self.lbl_x_wins, self.lbl_games_tied]:
            label.setAlignment(QtCore.Qt.AlignRight)
            label.setMinimumWidth(60)

        #%% Axes
        # board
        fig = Figure(figsize=(4.2, 4.2), dpi=100, frameon=False)
        self.board_canvas = FigureCanvas(fig)
        self.board_canvas.mpl_connect(
            'button_release_event',
            lambda event: self.mouse_click_callback(event))
        self.board_canvas.setMinimumSize(420, 420)
        self.board_axes = Axes(fig, [0., 0., 1., 1.])
        self.board_axes.invert_yaxis()
        fig.add_axes(self.board_axes)

        # current move
        fig = Figure(figsize=(1.05, 1.05), dpi=100, frameon=False)
        self.move_canvas = FigureCanvas(fig)
        self.move_canvas.setMinimumSize(105, 105)
        self.move_axes = Axes(fig, [0., 0., 1., 1.])
        self.move_axes.set_xlim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_ylim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_axis_off()
        fig.add_axes(self.move_axes)

        #%% Buttons
        # Undo button
        self.btn_undo = QPushButton('Undo')
        self.btn_undo.setToolTip('Undoes the last move.')
        self.btn_undo.setMinimumSize(60, 30)
        self.btn_undo.setStyleSheet(
            'color: yellow; background-color: #990000; font: bold;')
        self.btn_undo.clicked.connect(self.btn_undo_function)
        # New Game button
        self.btn_new = QPushButton('New Game')
        self.btn_new.setToolTip('Starts a new game.')
        self.btn_new.setMinimumSize(60, 50)
        self.btn_new.setStyleSheet(
            'color: yellow; background-color: #006633; font: bold;')
        self.btn_new.clicked.connect(self.btn_new_function)
        # Redo button
        self.btn_redo = QPushButton('Redo')
        self.btn_redo.setToolTip('Redoes the last move.')
        self.btn_redo.setMinimumSize(60, 30)
        self.btn_redo.setStyleSheet(
            'color: yellow; background-color: #000099; font: bold;')
        self.btn_redo.clicked.connect(self.btn_redo_function)

        for btn in [self.btn_undo, self.btn_new, self.btn_redo]:
            not_resize = btn.sizePolicy()
            not_resize.setRetainSizeWhenHidden(True)
            btn.setSizePolicy(not_resize)

        #%% Populate Widgets
        # score
        layout_score.addWidget(lbl_score, 0, 0, 1, 2)
        layout_score.addWidget(lbl_o, 1, 0)
        layout_score.addWidget(self.lbl_o_wins, 1, 1)
        layout_score.addWidget(lbl_x, 2, 0)
        layout_score.addWidget(self.lbl_x_wins, 2, 1)
        layout_score.addWidget(lbl_games, 3, 0)
        layout_score.addWidget(self.lbl_games_tied)

        # move
        layout_move.addWidget(lbl_move)
        layout_move.addWidget(self.move_canvas)

        # buttons
        layout_buttons.addWidget(self.btn_undo)
        layout_buttons.addWidget(self.btn_new)
        layout_buttons.addWidget(self.btn_redo)

        # board
        layout_board.addWidget(self.board_canvas)
        layout_board.addWidget(self.grp_buttons)

        # main
        layout_main.addWidget(self.grp_score)
        layout_main.addWidget(self.grp_board)
        layout_main.addWidget(self.grp_move)

        # main GUI
        layout_gui.addWidget(lbl_tictactoe)
        layout_gui.addWidget(self.grp_main)

        #%% File Menu
        # actions - new game
        act_new_game = QAction('New Game', self)
        act_new_game.setShortcut('Ctrl+N')
        act_new_game.setStatusTip('Starts a new game.')
        act_new_game.triggered.connect(self.act_new_game_func)
        # actions - options
        act_options = QAction('Options', self)
        act_options.setShortcut('Ctrl+O')
        act_options.setStatusTip('Opens the advanced option settings.')
        act_options.triggered.connect(self.act_options_func)
        # actions - quit game
        act_quit = QAction('Exit', self)
        act_quit.setShortcut('Ctrl+Q')
        act_quit.setStatusTip('Exits the application.')
        act_quit.triggered.connect(self.close)

        # menubar
        self.statusBar()
        menu_bar = self.menuBar()
        file_menu = menu_bar.addMenu('&File')
        file_menu.addAction(act_new_game)
        file_menu.addAction(act_options)
        file_menu.addAction(act_quit)

        #%% Finalization
        # Call wrapper to initialize GUI
        self.wrapper()

        # GUI final layout properties
        self.center()
        self.setWindowTitle('Tic Tac Toe')
        self.setWindowIcon(
            QtGui.QIcon(os.path.join(get_images_dir(), 'tictactoe.png')))
        self.show()

    #%% Other callbacks - closing
    def closeEvent(self, event):
        r"""Things in here happen on GUI closing."""
        filename = os.path.join(get_output_dir(), 'tictactoe.pkl')
        GameStats.save(filename, self.state.game_hist)
        event.accept()

    #%% Other callbacks - center the GUI on the screen
    def center(self):
        r"""Makes the GUI centered on the active screen."""
        frame_gm = self.frameGeometry()
        screen = QApplication.desktop().screenNumber(
            QApplication.desktop().cursor().pos())
        center_point = QApplication.desktop().screenGeometry(screen).center()
        frame_gm.moveCenter(center_point)
        self.move(frame_gm.topLeft())

    #%% Other callbacks - setup_axes
    def setup_axes(self):
        r"""Sets up the axes for the board and move."""
        # setup board axes
        self.board_axes.clear()
        if np.less(*self.board_axes.get_ylim()):
            self.board_axes.invert_yaxis()
        self.board_axes.set_xlim(-SIZES['square'] / 2,
                                 SIZES['board'] - SIZES['square'] / 2)
        self.board_axes.set_ylim(SIZES['board'] - SIZES['square'] / 2,
                                 -SIZES['square'] / 2)
        self.board_axes.set_aspect('equal')
        # setup move axes
        self.move_axes.clear()
        self.move_axes.set_xlim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_ylim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_aspect('equal')

    #%% Other callbacks - display_controls
    def display_controls(self):
        r"""Determines what controls to display on the GUI."""
        # show/hide New Game Button
        if self.state.game_hist[self.state.cur_game].winner == PLAYER['none']:
            self.btn_new.hide()
        else:
            self.btn_new.show()

        # show/hide Undo Button
        if self.state.cur_move > 0:
            self.btn_undo.show()
        else:
            self.btn_undo.hide()

        # show/hide Redo Button
        if self.state.game_hist[
                self.state.cur_game].num_moves > self.state.cur_move:
            self.btn_redo.show()
        else:
            self.btn_redo.hide()

    #%% Other callbacks - update_game_stats
    def update_game_stats(self, results):
        r"""Updates the game stats on the left of the GUI."""
        # calculate the number of wins
        o_wins = np.count_nonzero(results == PLAYER['o'])
        x_wins = np.count_nonzero(results == PLAYER['x'])
        games_tied = np.count_nonzero(results == PLAYER['draw'])

        # update the gui
        self.lbl_o_wins.setText("{}".format(o_wins))
        self.lbl_x_wins.setText("{}".format(x_wins))
        self.lbl_games_tied.setText("{}".format(games_tied))

    #%% Button callbacks
    def btn_undo_function(self):  # TODO: deal with AI moves, too
        r"""Function that executes on undo button press."""
        # get last move
        last_move = self.state.game_hist[self.state.cur_game].move_list[
            self.state.cur_move - 1]
        logger.debug(f'Undoing move = {last_move}')
        # delete piece
        self.state.board[last_move.row, last_move.column] = PLAYER['none']
        # update current move
        self.state.cur_move -= 1
        # call GUI wrapper
        self.wrapper()

    def btn_new_function(self):
        r"""Function that executes on new game button press."""
        # update values
        last_lead = self.state.game_hist[self.state.cur_game].first_move
        next_lead = PLAYER['x'] if last_lead == PLAYER['o'] else PLAYER['o']
        assert len(self.state.game_hist) == self.state.cur_game + 1
        self.state.cur_game += 1
        self.state.cur_move = Counter(0)
        self.state.game_hist.append(GameStats(number=self.state.cur_game, first_move=next_lead, \
            winner=PLAYER['none']))
        self.state.board = np.full((SIZES['board'], SIZES['board']),
                                   PLAYER['none'],
                                   dtype=int)
        # call GUI wrapper
        self.wrapper()

    def btn_redo_function(self):
        r"""Function that executes on redo button press."""
        # get next move
        redo_move = self.state.game_hist[self.state.cur_game].move_list[
            self.state.cur_move]
        logger.debug(f'Redoing move = {redo_move}')
        # place piece
        self.state.board[redo_move.row, redo_move.column] = calc_cur_move(self.state.cur_move, \
            self.state.cur_game)
        # update current move
        self.state.cur_move += 1
        # call GUI wrapper
        self.wrapper()

    #%% Menu action callbacks
    def act_new_game_func(self):
        r"""Function that executes on new game menu selection."""
        self.btn_new_function()

    def act_options_func(self):
        r"""Function that executes on options menu selection."""
        pass  # TODO: write this

    #%% mouse_click_callback
    def mouse_click_callback(self, event):
        r"""Function that executes on mouse click on the board axes.  Ends up placing a piece on the board."""
        # ignore events that are outside the axes
        if event.xdata is None or event.ydata is None:
            logger.debug('Click is off the board.')
            return
        # test for a game that has already been concluded
        if self.state.game_hist[self.state.cur_game].winner != PLAYER['none']:
            logger.debug('Game is over.')
            return
        # alias the rounded values of the mouse click location
        x = np.round(event.ydata).astype(int)
        y = np.round(event.xdata).astype(int)
        logger.debug(f'Clicked on (x,y) = ({x}, {y})')
        # get axes limits
        (m, n) = self.state.board.shape
        # ignore values that are outside the board
        if x < 0 or y < 0 or x >= m or y >= n:  # pragma: no cover
            logger.debug('Click is outside playable board.')
            return
        # check that move is on a free square
        if self.state.board[x, y] == PLAYER['none']:
            # make the move
            make_move(self.board_axes, self.state.board, x, y, self.state.cur_move, \
                self.state.cur_game, self.state.game_hist)
        # redraw the game/board
        self.wrapper()

    #%% wrapper
    def wrapper(self):
        r"""Acts as a wrapper to everything the GUI needs to do."""
        def sub_wrapper(self):
            r"""Sub-wrapper so that the wrapper can call itself for making AI moves."""
            # clean up an existing artifacts and reset axes
            self.setup_axes()

            # plot the current move
            current_player = calc_cur_move(self.state.cur_move,
                                           self.state.cur_game)
            plot_cur_move(self.move_axes, current_player)
            self.move_canvas.draw()

            # draw the board
            plot_board(self.board_axes, self.state.board)

            # check for win
            (winner, win_mask) = check_for_win(self.state.board)
            # update winner
            self.state.game_hist[self.state.cur_game].winner = winner

            # plot win
            plot_win(self.board_axes, win_mask, self.state.board)

            # display relevant controls
            self.display_controls()

            # find the best moves
            if winner == PLAYER['none'] and (OPTS.plot_best_moves
                                             or OPTS.plot_move_power):
                (o_moves, x_moves) = find_moves(self.state.board)

            # plot possible winning moves (includes updating turn arrows)
            if winner == PLAYER['none'] and OPTS.plot_best_moves:
                plot_possible_win(self.board_axes, o_moves, x_moves)

            # plot the move power
            if winner == PLAYER['none'] and OPTS.plot_move_power:
                plot_powers(self.board_axes, self.state.board, o_moves,
                            x_moves)

            # redraw with the final board
            self.board_axes.set_axis_off()
            self.board_canvas.draw()

            # update game stats on GUI
            self.update_game_stats(
                results=GameStats.get_results(self.state.game_hist))
            self.update()

            return (winner, current_player)

        # call the wrapper
        (winner, current_player) = sub_wrapper(self)

        # make computer AI move
        while winner == PLAYER['none'] and (\
            (OPTS.o_is_computer and current_player == PLAYER['o']) or \
            (OPTS.x_is_computer and current_player == PLAYER['x'])):
            play_ai_game(self.board_axes, self.state.board, self.state.cur_move, \
                self.state.cur_game, self.state.game_hist)
            (winner, current_player) = sub_wrapper(self)
示例#10
0
class PentagoGui(QWidget):
    r"""The Pentago GUI."""
    def __init__(self, filename=None, **kwargs):
        # call super method
        super().__init__(**kwargs)
        # initialize the state data
        self.initialize_state(filename=filename)
        # load the image data
        self.load_images()
        # call init method to instantiate the GUI
        self.init()

    #%% State initialization
    def initialize_state(self, filename):
        r"""Loads the previous game based on settings and whether the file exists."""
        # preallocate to not load
        load_game = False
        if OPTIONS['load_previous_game'] == 'No':
            pass
        elif OPTIONS['load_previous_game'] == 'Yes':
            load_game = True
        # ask if loading
        elif OPTIONS['load_previous_game'] == 'Ask':
            widget = QWidget()
            reply = QMessageBox.question(widget, 'Message', \
                "Do you want to load the previous game?", QMessageBox.Yes | \
                QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                load_game = True
        else:
            raise ValueError(
                'Unexpected value for the load_previous_game option.')
        # initialize outputs
        self.state = State()
        # load previous game
        if load_game:
            if filename is None:
                filename = os.path.join(get_output_dir(), 'pentago.pkl')
            if os.path.isfile(filename):
                self.state.game_hist = GameStats.load(filename)
                self.state.cur_game = Counter(len(self.state.game_hist) - 1)
                self.state.cur_move = Counter(
                    len(self.state.game_hist[-1].move_list))
                self.state.board       = create_board_from_moves(self.state.game_hist[-1].move_list, \
                    self.state.game_hist[-1].first_move)
                self.state.move_status = {
                    'ok': False,
                    'pos': None,
                    'patch_object': None
                }
            else:
                raise ValueError(
                    f'Could not find file: "{filename}"')  # pragma: no cover

    #%% load_images
    def load_images(self):
        r"""
        Loads the images for use later on.

        Returns
        -------
        images : dict
            Images for use on the rotation buttons.

        Notes
        -----
        #.  Written by David C. Stauffer in January 2016.
        #.  TODO: needs a QApplication to exist first.  Play around with making this earlier.

        """
        # get the directory for the images
        images_dir = get_images_dir()
        # create a dictionary for saving all the images in
        self.images = {}
        self.images['1R'] = QtGui.QIcon(os.path.join(images_dir, 'right1.png'))
        self.images['2R'] = QtGui.QIcon(os.path.join(images_dir, 'right2.png'))
        self.images['3R'] = QtGui.QIcon(os.path.join(images_dir, 'right3.png'))
        self.images['4R'] = QtGui.QIcon(os.path.join(images_dir, 'right4.png'))
        self.images['1L'] = QtGui.QIcon(os.path.join(images_dir, 'left1.png'))
        self.images['2L'] = QtGui.QIcon(os.path.join(images_dir, 'left2.png'))
        self.images['3L'] = QtGui.QIcon(os.path.join(images_dir, 'left3.png'))
        self.images['4L'] = QtGui.QIcon(os.path.join(images_dir, 'left4.png'))
        self.images['wht'] = QtGui.QIcon(
            os.path.join(images_dir, 'blue_button.png'))
        self.images['blk'] = QtGui.QIcon(
            os.path.join(images_dir, 'cyan_button.png'))
        self.images['w_b'] = QtGui.QIcon(
            os.path.join(images_dir, 'blue_cyan_button.png'))
        self.images['b_w'] = QtGui.QIcon(
            os.path.join(images_dir, 'cyan_blue_button.png'))

    #%% GUI initialization
    def init(self):
        r"""Initializes the GUI."""
        #%% properties
        QToolTip.setFont(QtGui.QFont('SansSerif', 10))

        #%% Text
        # Pentago
        lbl_pentago = QLabel('Pentago', self)
        lbl_pentago.setGeometry(390, 50, 220, 40)
        lbl_pentago.setAlignment(QtCore.Qt.AlignCenter)
        lbl_pentago.setStyleSheet('font-size: 18pt; font: bold;')
        # Score
        lbl_score = QLabel('Score:', self)
        lbl_score.setGeometry(35, 220, 220, 40)
        lbl_score.setAlignment(QtCore.Qt.AlignCenter)
        lbl_score.setStyleSheet('font-size: 12pt; font: bold;')
        # Move
        lbl_move = QLabel('Move:', self)
        lbl_move.setGeometry(725, 220, 220, 40)
        lbl_move.setAlignment(QtCore.Qt.AlignCenter)
        lbl_move.setStyleSheet('font-size: 12pt; font: bold;')
        # White Wins
        lbl_white = QLabel('White Wins:', self)
        lbl_white.setGeometry(80, 280, 80, 20)
        # Black Wins
        lbl_black = QLabel('Black Wins:', self)
        lbl_black.setGeometry(80, 310, 80, 20)
        # Games Tied
        lbl_games = QLabel('Games Tied:', self)
        lbl_games.setGeometry(80, 340, 80, 20)
        # Changeable labels
        self.lbl_white_wins = QLabel('0', self)
        self.lbl_white_wins.setGeometry(140, 280, 60, 20)
        self.lbl_white_wins.setAlignment(QtCore.Qt.AlignRight)
        self.lbl_black_wins = QLabel('0', self)
        self.lbl_black_wins.setGeometry(140, 310, 60, 20)
        self.lbl_black_wins.setAlignment(QtCore.Qt.AlignRight)
        self.lbl_games_tied = QLabel('0', self)
        self.lbl_games_tied.setGeometry(140, 340, 60, 20)
        self.lbl_games_tied.setAlignment(QtCore.Qt.AlignRight)

        #%% Axes
        # board
        self.wid_board = QWidget(self)
        self.wid_board.setGeometry(290, 140, 420, 420)
        fig = Figure(figsize=(4.2, 4.2), dpi=100, frameon=False)
        self.board_canvas = FigureCanvas(fig)
        self.board_canvas.setParent(self.wid_board)
        self.board_canvas.mpl_connect(
            'button_release_event',
            lambda event: self.mouse_click_callback(event))
        self.board_axes = Axes(fig, [0., 0., 1., 1.])
        self.board_axes.invert_yaxis()
        #self.board_axes.set_axis_off() # TODO: do I need this line, or handled in self.setup_axes()?
        fig.add_axes(self.board_axes)

        # current move
        self.wid_move = QWidget(self)
        self.wid_move.setGeometry(800, 280, 70, 70)
        fig = Figure(figsize=(.7, .7), dpi=100, frameon=False)
        self.move_canvas = FigureCanvas(fig)
        self.move_canvas.setParent(self.wid_move)
        self.move_axes = Axes(fig, [0., 0., 1., 1.])
        self.move_axes.set_xlim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_ylim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_axis_off()
        fig.add_axes(self.move_axes)

        #%% Buttons
        button_size = QtCore.QSize(SIZES['button'], SIZES['button'])
        # Undo button
        self.btn_undo = QPushButton('Undo', self)
        self.btn_undo.setToolTip('Undoes the last move.')
        self.btn_undo.setGeometry(380, 600, 60, 30)
        self.btn_undo.setStyleSheet(
            'color: yellow; background-color: #990000; font: bold;')
        self.btn_undo.clicked.connect(self.btn_undo_function)
        # New Game button
        self.btn_new = QPushButton('New Game', self)
        self.btn_new.setToolTip('Starts a new game.')
        self.btn_new.setGeometry(460, 600, 80, 50)
        self.btn_new.setStyleSheet(
            'color: yellow; background-color: #006633; font: bold;')
        self.btn_new.clicked.connect(self.btn_new_function)
        # Redo button
        self.btn_redo = QPushButton('Redo', self)
        self.btn_redo.setToolTip('Redoes the last move.')
        self.btn_redo.setGeometry(560, 600, 60, 30)
        self.btn_redo.setStyleSheet(
            'color: yellow; background-color: #000099; font: bold;')
        self.btn_redo.clicked.connect(self.btn_redo_function)

        # 1R button
        self.btn_1R = RotationButton('', self, quadrant=1, direction=1)
        self.btn_1R.setToolTip('Rotates quadrant 1 to the right 90 degrees.')
        self.btn_1R.setIconSize(button_size)
        self.btn_1R.setGeometry(290, 49, SIZES['button'], SIZES['button'])
        self.btn_1R.clicked.connect(self.btn_rot_function)
        # 2R button
        self.btn_2R = RotationButton('', self, quadrant=2, direction=1)
        self.btn_2R.setToolTip('Rotates quadrant 2 to the right 90 degrees.')
        self.btn_2R.setIconSize(button_size)
        self.btn_2R.setGeometry(730, 139, SIZES['button'], SIZES['button'])
        self.btn_2R.clicked.connect(self.btn_rot_function)
        # 3R button
        self.btn_3R = RotationButton('', self, quadrant=3, direction=1)
        self.btn_3R.setToolTip('Rotates quadrant 3 to the right 90 degrees.')
        self.btn_3R.setIconSize(button_size)
        self.btn_3R.setGeometry(200, 489, SIZES['button'], SIZES['button'])
        self.btn_3R.clicked.connect(self.btn_rot_function)
        # 4R button
        self.btn_4R = RotationButton('', self, quadrant=4, direction=1)
        self.btn_4R.setToolTip('Rotates quadrant 4 to the right 90 degrees.')
        self.btn_4R.setIconSize(button_size)
        self.btn_4R.setGeometry(640, 579, SIZES['button'], SIZES['button'])
        self.btn_4R.clicked.connect(self.btn_rot_function)
        # 1L button
        self.btn_1L = RotationButton('', self, quadrant=1, direction=-1)
        self.btn_1L.setToolTip('Rotates quadrant 1 to the left 90 degrees.')
        self.btn_1L.setIconSize(button_size)
        self.btn_1L.setGeometry(200, 139, SIZES['button'], SIZES['button'])
        self.btn_1L.clicked.connect(self.btn_rot_function)
        # 2L button
        self.btn_2L = RotationButton('', self, quadrant=2, direction=-1)
        self.btn_2L.setToolTip('Rotates quadrant 2 to the left 90 degrees.')
        self.btn_2L.setIconSize(button_size)
        self.btn_2L.setGeometry(640, 49, SIZES['button'], SIZES['button'])
        self.btn_2L.clicked.connect(self.btn_rot_function)
        # 3L button
        self.btn_3L = RotationButton('', self, quadrant=3, direction=-1)
        self.btn_3L.setToolTip('Rotates quadrant 3 to the left 90 degrees.')
        self.btn_3L.setIconSize(button_size)
        self.btn_3L.setGeometry(290, 579, SIZES['button'], SIZES['button'])
        self.btn_3L.clicked.connect(self.btn_rot_function)
        # 4L button
        self.btn_4L = RotationButton('', self, quadrant=4, direction=-1)
        self.btn_4L.setToolTip('Rotates quadrant 4 to the left 90 degrees.')
        self.btn_4L.setIconSize(button_size)
        self.btn_4L.setGeometry(730, 489, SIZES['button'], SIZES['button'])
        self.btn_4L.clicked.connect(self.btn_rot_function)
        # buttons dictionary for use later
        self.rot_buttons = {'1L':self.btn_1L, '2L':self.btn_2L, '3L':self.btn_3L, '4L':self.btn_4L, \
            '1R':self.btn_1R, '2R':self.btn_2R, '3R':self.btn_3R, '4R':self.btn_4R}

        #%% Finalization
        # Call wrapper to initialize GUI
        self.wrapper()

        # GUI final layout properties
        self.resize(1000, 700)
        self.center()
        self.setWindowTitle('Pentago')
        self.setWindowIcon(
            QtGui.QIcon(os.path.join(get_images_dir(), 'pentago.png')))
        self.show()

    #%% Other callbacks - closing
    def closeEvent(self, event):
        r"""Things in here happen on GUI closing."""
        close_immediately = True
        filename = os.path.join(get_output_dir(), 'pentago.pkl')
        if close_immediately:
            GameStats.save(filename, self.state.game_hist)
            event.accept()
        else:
            # Alternative with user choice
            reply = QMessageBox.question(self, 'Message', \
                "Are you sure to quit?", QMessageBox.Yes | \
                QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                GameStats.save(filename, self.state.game_hist)
                event.accept()
            else:
                event.ignore()

    #%% Other callbacks - center the GUI on the screen
    def center(self):
        r"""Makes the GUI centered on the active screen."""
        frame_gm = self.frameGeometry()
        screen = QApplication.desktop().screenNumber(
            QApplication.desktop().cursor().pos())
        center_point = QApplication.desktop().screenGeometry(screen).center()
        frame_gm.moveCenter(center_point)
        self.move(frame_gm.topLeft())

    #%% Other callbacks - setup_axes
    def setup_axes(self):
        r"""Sets up the axes for the board and move."""
        # setup board axes
        self.board_axes.clear()
        if np.less(*self.board_axes.get_ylim()):
            self.board_axes.invert_yaxis()
        self.board_axes.set_xlim(-SIZES['square'] / 2,
                                 SIZES['board'] - SIZES['square'] / 2)
        self.board_axes.set_ylim(SIZES['board'] - SIZES['square'] / 2,
                                 -SIZES['square'] / 2)
        self.board_axes.set_aspect('equal')
        # setup move axes
        self.move_axes.clear()
        self.move_axes.set_xlim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_ylim(-SIZES['square'] / 2, SIZES['square'] / 2)
        self.move_axes.set_aspect('equal')

    #%% Other callbacks - display_controls
    def display_controls(self):
        r"""Determines what controls to display on the GUI."""
        # show/hide New Game Button
        if self.state.game_hist[self.state.cur_game].winner == PLAYER['none']:
            self.btn_new.hide()
        else:
            self.btn_new.show()

        # show/hide Undo Button
        if self.state.cur_move > 0:
            self.btn_undo.show()
        else:
            self.btn_undo.hide()

        # show/hide Redo Button
        if self.state.game_hist[
                self.state.cur_game].num_moves > self.state.cur_move:
            self.btn_redo.show()
        else:
            self.btn_redo.hide()

    #%% Other callbacks - update_game_stats
    def update_game_stats(self, results):
        r"""Updates the game stats on the left of the GUI."""
        # calculate the number of wins
        white_wins = np.count_nonzero(results == PLAYER['white'])
        black_wins = np.count_nonzero(results == PLAYER['black'])
        games_tied = np.count_nonzero(results == PLAYER['draw'])

        # update the gui
        self.lbl_white_wins.setText("{}".format(white_wins))
        self.lbl_black_wins.setText("{}".format(black_wins))
        self.lbl_games_tied.setText("{}".format(games_tied))

    #%% Button callbacks
    def btn_undo_function(self):
        r"""Function that executes on undo button press."""
        # get last move
        last_move = self.state.game_hist[self.state.cur_game].move_list[
            self.state.cur_move - 1]
        logger.debug(f'Undoing move = {last_move}')
        # undo rotation
        rotate_board(self.state.board, last_move.quadrant,
                     -last_move.direction)
        # delete piece
        self.state.board[last_move.row, last_move.column] = PLAYER['none']
        # update current move
        self.state.cur_move -= 1
        # call GUI wrapper
        self.wrapper()

    def btn_new_function(self):
        r"""Function that executes on new game button press."""
        # update values
        last_lead = self.state.game_hist[self.state.cur_game].first_move
        next_lead = PLAYER['black'] if last_lead == PLAYER[
            'white'] else PLAYER['white']
        assert len(self.state.game_hist) == self.state.cur_game + 1
        self.state.cur_game += 1
        self.state.cur_move = Counter(0)
        self.state.game_hist.append(GameStats(number=self.state.cur_game, first_move=next_lead, \
            winner=PLAYER['none']))
        self.state.board = np.full((SIZES['board'], SIZES['board']),
                                   PLAYER['none'],
                                   dtype=int)
        # call GUI wrapper
        self.wrapper()

    def btn_redo_function(self):
        r"""Function that executes on redo button press."""
        # get next move
        redo_move = self.state.game_hist[self.state.cur_game].move_list[
            self.state.cur_move]
        logger.debug(f'Redoing move = {redo_move}')
        # place piece
        self.state.board[redo_move.row, redo_move.column] = calc_cur_move(self.state.cur_move, \
            self.state.cur_game)
        # redo rotation
        rotate_board(self.state.board, redo_move.quadrant, redo_move.direction)
        # update current move
        self.state.cur_move += 1
        # call GUI wrapper
        self.wrapper()

    def btn_rot_function(self):
        r"""Functions that executes on rotation button press."""
        # determine sending button
        button = self.sender()
        # execute the move
        self.execute_move(quadrant=button.quadrant, direction=button.direction)
        # call GUI wrapper
        self.wrapper()

    #%% Menu action callbacks
    pass  # TODO: write this

    #%% mouse_click_callback
    def mouse_click_callback(self, event):
        r"""Function that executes on mouse click on the board axes.  Ends up placing a piece on the board."""
        # ignore events that are outside the axes
        if event.xdata is None or event.ydata is None:
            logger.debug('Click is off the board.')
            return
        # test for a game that has already been concluded
        if self.state.game_hist[self.state.cur_game].winner != PLAYER['none']:
            logger.debug('Game is over.')
            self.state.move_status['ok'] = False
            self.state.move_status['pos'] = None
            return
        # alias the rounded values of the mouse click location
        x = np.round(event.ydata).astype(int)
        y = np.round(event.xdata).astype(int)
        logger.debug(f'Clicked on (x,y) = ({x}, {y})')
        # get axes limits
        (m, n) = self.state.board.shape
        # ignore values that are outside the board
        if x < 0 or y < 0 or x >= m or y >= n:
            logger.debug('Click is outside playable board.')
            return
        if self.state.board[x, y] == PLAYER['none']:
            # check for previous good move
            if self.state.move_status['ok']:
                logger.debug('removing previous piece.')
                self.state.move_status['patch_object'].remove()
            self.state.move_status['ok'] = True
            self.state.move_status['pos'] = (x, y)
            logger.debug('Placing current piece.')
            current_player = calc_cur_move(self.state.cur_move,
                                           self.state.cur_game)
            if current_player == PLAYER['white']:
                self.state.move_status['patch_object'] = plot_piece(
                    self.board_axes, x, y, SIZES['piece'], COLOR['next_wht'])
            elif current_player == PLAYER['black']:
                self.state.move_status['patch_object'] = plot_piece(
                    self.board_axes, x, y, SIZES['piece'], COLOR['next_blk'])
            else:
                raise ValueError('Unexpected player to move next.')
        else:
            # delete a previously placed piece
            if self.state.move_status['ok']:
                self.state.move_status['patch_object'].remove()
            self.state.move_status['ok'] = False
            self.state.move_status['pos'] = None
        # redraw the board
        self.board_canvas.draw()

    #%% execute_move
    def execute_move(self, *, quadrant, direction):
        r"""Tests and then executes a move."""
        if self.state.move_status['ok']:
            logger.debug('Rotating Quadrant {} in Direction {}.'.format(
                quadrant, direction))
            # delete gray piece
            self.state.move_status['patch_object'].remove()
            self.state.move_status['patch_object'] = None
            # add new piece to board
            self.state.board[self.state.move_status['pos'][0], self.state.move_status['pos'][1]] = \
                calc_cur_move(self.state.cur_move, self.state.cur_game)
            # rotate board
            rotate_board(self.state.board, quadrant, direction)
            # increment move list
            assert self.state.game_hist[self.state.cur_game].num_moves >= self.state.cur_move, \
                'Number of moves = {}, Current Move = {}'.format(self.state.game_hist[self.state.cur_game].num_moves, self.state.cur_move)
            this_move = Move(self.state.move_status['pos'][0],
                             self.state.move_status['pos'][1], quadrant,
                             direction)
            if self.state.game_hist[
                    self.state.cur_game].num_moves == self.state.cur_move:
                self.state.game_hist[self.state.cur_game].add_move(this_move)
            else:
                self.state.game_hist[self.state.cur_game].move_list[
                    self.state.cur_move] = this_move
                self.state.game_hist[self.state.cur_game].remove_moves(
                    self.state.cur_move + 1)
            # increment current move
            self.state.cur_move += 1
            # reset status for next move
            self.state.move_status['ok'] = False
            self.state.move_status['pos'] = None
        else:
            logger.debug('No move to execute.')

    #%% wrapper
    def wrapper(self):
        r"""Acts as a wrapper to everything the GUI needs to do."""
        # clean up an existing artifacts
        self.setup_axes()

        # plot the current move
        plot_cur_move(self.move_axes,
                      calc_cur_move(self.state.cur_move, self.state.cur_game))
        self.move_canvas.draw()

        # draw turn arrows in default colors
        for button in self.rot_buttons.values():
            button.overlay = None

        # draw the board
        plot_board(self.board_axes, self.state.board)

        # check for win
        (winner, win_mask) = check_for_win(self.state.board)
        # update winner
        self.state.game_hist[self.state.cur_game].winner = winner

        # plot win
        plot_win(self.board_axes, win_mask)

        # display relevant controls
        self.display_controls()

        # plot possible winning moves (includes updating turn arrows)
        if winner == PLAYER['none'] and OPTIONS['plot_winning_moves']:
            (white_moves, black_moves) = find_moves(self.state.board)
            plot_possible_win(self.board_axes, self.rot_buttons, white_moves,
                              black_moves, self.state.cur_move,
                              self.state.cur_game)

        # redraw with the final board
        self.board_axes.set_axis_off()
        self.board_canvas.draw()

        # update game stats on GUI
        self.update_game_stats(
            results=GameStats.get_results(self.state.game_hist))
        self.update()