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)
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()
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()
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)
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()
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
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")
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
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)
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()