How to display the winning move in Tic Tac Toe PyQT5?

Viewed 206

I'm currently familiarizing myself with reinforcement learning basics and for convenience purposes (instead of manually entering coordinates in the terminal), I created a very simple UI for testing trained agents and play games. For testing purposes, I created a method that generates a random move after the human player clicks on a cell and each move is displayed after the click / random move however when the move is a winning move it does not show up and the board resets. I don't know why this is happening but it looks like the order of execution is not sequential/there is something I don't understand. I tried using time.sleep(1) after the board update to ensure I do not overlook the move being actually displayed however the move never gets to show, the window just freezes.

Expected outcome:

ex

What I actually get(given I'll click on cell (0, 2)):

ac

from PyQt5.QtWidgets import (
    QMainWindow,
    QDesktopWidget,
    QApplication,
    QWidget,
    QHBoxLayout,
    QVBoxLayout,
    QPushButton,
    QLabel,
)
from PyQt5.QtCore import Qt
import numpy as np
import sys


class TicCell(QPushButton):
    """
    Tic Tac Toe cell.
    """

    def __init__(self, location):
        """
        Initialize cell location.
        Args:
            location: Tuple, (row, col).
        """
        super().__init__()
        self.location = location


class TicUI(QMainWindow):
    """
    Tic Tac Toe interface.
    """

    def __init__(
        self,
        window_title='Smart Tic Tac Toe',
        board_size=3,
        empty_value=0,
        x_value=1,
        o_value=2,
        agent=None,
    ):
        """
        Initialize game settings.
        Args:
            window_title: Display window name.
            board_size: int, the board will be of size(board_size, board_size).
            empty_value: int representation of an empty cell.
            x_value: int representation of a cell containing X.
            o_value: int representation of a cell containing O.
            agent: Trained TicAgent object.
        """
        super().__init__()
        self.setWindowTitle(window_title)
        self.board_size = board_size
        self.empty_value = empty_value
        self.x_value = x_value
        self.o_value = o_value
        self.agent = agent
        self.text_map = {
            x_value: 'X',
            o_value: 'O',
            empty_value: '',
            'X': x_value,
            'O': o_value,
            '': empty_value,
        }
        win_rectangle = self.frameGeometry()
        center_point = QDesktopWidget().availableGeometry().center()
        win_rectangle.moveCenter(center_point)
        self.central_widget = QWidget(self)
        self.main_layout = QVBoxLayout()
        self.score_layout = QHBoxLayout()
        self.human_score = 0
        self.agent_score = 0
        self.score_board = QLabel()
        self.update_score_board()
        self.setStyleSheet('QPushButton:!hover {color: yellow}')
        self.cells = [
            [TicCell((c, r)) for r in range(board_size)]
            for c in range(board_size)
        ]
        self.cell_layouts = [QHBoxLayout() for _ in self.cells]
        self.board = np.ones((board_size, board_size)) * self.empty_value
        self.adjust_layouts()
        self.adjust_cells()
        self.update_cell_values()
        self.show()

    def adjust_layouts(self):
        """
        Adjust score board and cell layouts.

        Returns:
            None
        """
        self.main_layout.addLayout(self.score_layout)
        for cell_layout in self.cell_layouts:
            self.main_layout.addLayout(cell_layout)
        self.central_widget.setLayout(self.main_layout)
        self.setCentralWidget(self.central_widget)

    def adjust_cells(self):
        """
        Adjust display cells.

        Returns:
            None
        """
        self.score_layout.addWidget(self.score_board)
        self.score_board.setAlignment(Qt.AlignCenter)
        for row_index, row in enumerate(self.cells):
            for cell in row:
                cell.setFixedSize(50, 50)
                cell.clicked.connect(self.game_step)
                self.cell_layouts[row_index].addWidget(cell)

    def get_empty_cells(self):
        """
        Get empty cell locations.

        Returns:
            A list of indices that reepresent currently empty cells.
        """
        empty_locations = np.where(self.board == self.empty_value)
        empty_locations = list(zip(empty_locations[0], empty_locations[1]))
        for empty_location in empty_locations:
            r, c = empty_location
            cell_text = self.cells[r][c].text()
            assert cell_text == self.text_map[self.empty_value], (
                f'location {empty_location} has a cell value of {cell_text}'
                f'and board value of {self.board[r][c]} {self.board}'
            )
        return empty_locations

    def update_score_board(self):
        """
        Update the display scores.

        Returns:
            None
        """
        self.score_board.setText(
            f'Human {self.human_score} - ' f'{self.agent_score} Agent'
        )

    def check_win(self, player_value):
        """
        Check current game state for winner.
        Args:
            player_value: int, self.x_value or self.o_value.

        Returns:
            True if player_value won, False otherwise.
        """
        return (
            np.all(self.board == player_value, axis=0).any()
            or np.all(self.board == player_value, axis=1).any()
            or np.all(self.board.diagonal() == player_value)
            or np.all(self.board[::-1].diagonal() == player_value)
        )

    def reset_cell_colors(self):
        """
        Reset display cell text colors.

        Returns:
            None
        """
        for row_idx in range(self.board_size):
            for col_idx in range(self.board_size):
                self.cells[row_idx][col_idx].setStyleSheet('color: yellow')

    def reset_game(self, winner=None):
        """
        Reset board and display cells and update display scores.
        Args:
            winner: int, self.x_value or self.o_value.

        Returns:
            None
        """
        self.board = (
            np.ones((self.board_size, self.board_size)) * self.empty_value
        )
        self.update_cell_values()
        if winner == self.x_value:
            self.human_score += 1
        if winner == self.o_value:
            self.agent_score += 1
        self.update_score_board()
        self.reset_cell_colors()

    def modify_step(self, cell_location, value):
        """
        Modify board and display cells.
        Args:
            cell_location: tuple, representing indices(row, col).
            value: int, self.x_value or self.o_value.

        Returns:
            True if the clicked cell is not empty, None otherwise.
        """
        r, c = cell_location
        board_value = self.board[r, c]
        cell_value = self.cells[r][c].text()
        if not board_value == self.empty_value:
            return True
        assert cell_value == self.text_map[self.empty_value], (
            f'mismatch between board value({board_value}) '
            f'and cell value({cell_value}) for location {(r, c)}'
        )
        if value == self.x_value:
            self.cells[r][c].setStyleSheet('color: red')
        self.board[r, c] = value
        self.cells[r][c].setText(f'{self.text_map[value]}')

    def game_step(self):
        """
        Post cell-click step(human step and agent step)

        Returns:
            None
        """
        cell = self.sender()
        stop = self.modify_step(cell.location, self.x_value)
        if stop:
            return
        x_win = self.check_win(self.x_value)
        if x_win:
            self.reset_game(self.x_value)
            return
        empty_locations = self.get_empty_cells()
        if not empty_locations:
            self.reset_game()
            return
        choice = np.random.choice(range(len(empty_locations)))
        if self.agent:
            choice = self.agent.generate_move(self.board, empty_locations)
        self.modify_step(empty_locations[choice], self.o_value)
        o_win = self.check_win(self.o_value)
        if o_win:
            self.reset_game(self.o_value)

    def update_cell_values(self):
        """
        Sync display cells with self.board

        Returns:
            None
        """
        for row_index, row in enumerate(self.board):
            for col_index, col in enumerate(row):
                update_value = self.text_map[self.board[row_index][col_index]]
                self.cells[row_index][col_index].setText(f'{update_value}')


if __name__ == '__main__':
    test = QApplication(sys.argv)
    window = TicUI()
    sys.exit(test.exec_())
1 Answers

In most GUI toolkits, you never actually draw to the screen directly. Instead, what's happening is that you are either drawing to a secondary buffer or you're only updating the widget's object model, which the toolkit then will copy to the actual screen when you hand control back to the toolkit's main loop.

If you want to update the actual interface, you'll need to return from the callback method. So if you want to update the screen and then update it again after a few seconds, you'll need to return from the first callback after setting up a timer to make a callback to do the second screen update. In QT, you can use QTimer for this.

Related