5
\$\begingroup\$

enter image description here

enter image description hereenter image description hereenter image description here

This is a huge project that I have worked on for the past few months, it contains 16 scripts, one of the scripts is used to analyze the other scripts. There are 111004 characters in all the script files, this exceeds the character limit of 65536 characters for a single post, and this figure doesn't include all the json files and pickle files needed to run the program.

So I will post this project in different questions. This question is about the UI, I will only post scripts related to the main window here, you won't be able to run the code. I have created a GitHub repository to host the project, in there you can find everything you need to run the project. The repository is here, if you download the repository, you need to first run analyze_tic_tac_toe_states.py once to generate the necessary data files. Then you can start the game by running main.py.

I have posted part 2, related to the styling, it is: GUI Tic-Tac-Toe game with six AI players - part 2: the styling

Part 3: GUI Tic-Tac-Toe game with AI, part 3: control and logic

You can see it in action here

I have also made a release containing created copies of the data files, but some of them are pickle files, I guarantee you that they don't contain malicious code, but they might be incompatible to your Python version. Mine is Python 3.10.11. You might need to run the aforementioned file once to generate data files. Release.

This is a Tic Tac Toe game with customizable GUI, it has six AI players to play against, and you can also set them against each other and watch them go at each other.

You can choose the shapes of the pieces representing the players, and their color, and also how the colors are applied to the shapes (blend modes). There are 24 shapes in total to choose from to use as playing pieces, and you can also choose the colors of the playing pieces, you can choose from all 16777216 RGB colors, and there are 27 blend modes.

The colors can be changed either by pressing the associated button to call up the dedicated dialog window, or using the specialized textbox above the buttons.

Color dialog:

enter image description here

You can also customize almost every widget via a dedicated Window:

enter image description here

You can change almost every part of the GUI, covering the text color, background color, gradient, and border style of every important widgets and you can also test your luck by pressing the Randomize button, every time it is clicked, it will randomly generate a new theme for every selected group of widgets, the colors are chosen completely at random from all 16777216 RGB colors, and border styles are randomly selected.

enter image description here

You can also revert the changes, and restore the settings to the default. It also has an animation, a thread that will cause many widgets to change their style and/or text and/or pictures every 125 milliseconds.


Scripts

main.py

import ctypes
import random
import sys
from advanced import *
from basics import *
from gamecontrol import STATSPATH
from preview import Preview
from PyQt6.QtGui import QCloseEvent
from PyQt6.QtTest import QTest
from PyQt6.QtWidgets import QApplication, QMainWindow
from shared import *
from theme import *


class Window(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        GLOBALS["Window"] = self
        self.initialized = False
        self.setFixedSize(622, 438)
        self.init_GUI()
        self.setup_widgets()
        self.add_widgets()
        self.setup_connections()
        self.setStyleSheet(STYLIZER.get_style())
        self.show()
        self.initialized = True
        GLOBALS["Game"].auto_start()
        if GLOBALS["Game"].match:
            self.popup_box.setDisabled(False)

    def init_GUI(self) -> None:
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.MSWindowsFixedSizeDialogHint
        )
        frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        frame.moveCenter(center)
        self.move(frame.topLeft())
        self.setWindowIcon(GLOBALS["ICON"])
        self.centralwidget = QWidget(self)
        self.setCentralWidget(self.centralwidget)
        self.setWindowTitle("Tic Tac Toe")

    def setup_widgets(self) -> None:
        self.vbox = make_vbox(self.centralwidget, 0)
        self.vbox.addWidget(TitleBar("Tic Tac Toe", exitgame))
        self.hbox = make_hbox()
        self.preview = Preview()
        GLOBALS["POPUP"] = self.popup_box = CheckBox("Popup messages")
        self.run = GLOBALS["rungamebutton"] = Button("Run")
        self.board = Board()
        self.player1 = Player("Player 1", "P1")
        self.player2 = Player("Player 2", "P2")
        self.statsbar = StatsBar()
        self.underbar = QHBoxLayout()
        self.customize_button = Button("Customize")

    def add_widgets(self) -> None:
        self.hbox.addWidget(self.board)
        self.hbox.addWidget(self.player1)
        self.hbox.addWidget(self.player2)
        self.vbox.addLayout(self.hbox)
        self.vbox.addWidget(self.statsbar)
        self.underbar.addWidget(self.popup_box)
        self.underbar.addStretch()
        self.underbar.addWidget(self.customize_button)
        self.underbar.addStretch()
        self.underbar.addWidget(self.run)
        self.vbox.addLayout(self.underbar)

    def setup_connections(self) -> None:
        self.popup_box.clicked.connect(self.set_popup)
        self.customize_button.clicked.connect(self.preview.show)
        self.run.clicked.connect(self.toggle)
        self.underbar.addWidget(self.run)

    def check_pairing(self) -> None:
        if (
            self.initialized
            and self.player1.item["Player"] == self.player2.item["Player"]
        ):
            if self.player1.changed:
                self.ensure_opponent(self.player1, self.player2)
            else:
                self.ensure_opponent(self.player2, self.player1)

    def ensure_opponent(self, new: Player, old: Player) -> None:
        new.changed = False
        box = old.player_choice_box
        box.currentTextChanged.disconnect(old.update_player)
        choice = random.choice(PLAYERS["opponents"][new.item["Player"]])
        old.player_choice_box.setCurrentText(choice)
        old.item["Player"] = choice
        old.set_player_info()
        box.currentTextChanged.connect(old.update_player)

    def toggle(self) -> None:
        self.run.setText(next(SWITCH))
        GLOBALS["rungame"] ^= 1
        if GLOBALS["rungame"]:
            GLOBALS["Game"].start()

    def set_popup(self) -> None:
        GLOBALS["popup"] = self.popup_box.isChecked()

    def closeEvent(self, e: QCloseEvent) -> None:
        exitgame()


def exitgame() -> None:
    GLOBALS["rungame"] = False
    GLOBALS["pause"] = True
    GLOBALS["run"] = False
    STATSPATH.write_text(json.dumps(GLOBALS["Game"].stats, indent=4))
    PLAYER_SETTINGS_PATH.write_text(json.dumps(PLAYER_SETTINGS, indent=4))
    Path(f"{FOLDER}/Data/menace-memory.pkl").write_bytes(
        pickle.dumps(MENACE_MEMORY, protocol=pickle.HIGHEST_PROTOCOL)
    )
    QTest.qWait(125)
    for window in QApplication.topLevelWidgets():
        window.close()


if __name__ == "__main__":
    app = QApplication([])
    app.setStyle("Fusion")
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("Tic Tac Toe")
    GLOBALS["ICON"] = QIcon(f"{FOLDER}/Icons/logo.png")
    GLOBALS["Logo"] = QPixmap(QImage(f"{FOLDER}/Icons/logo.png")).scaled(
        32, 32, Qt.AspectRatioMode.KeepAspectRatio
    )
    GLOBALS["Close"] = QIcon(f"{FOLDER}/Icons/close.png")
    GLOBALS["Minimize"] = QIcon(f"{FOLDER}/Icons/minimize.png")
    GLOBALS["win_emoji"] = QPixmap(QImage(f"{FOLDER}/Icons/win.png"))
    GLOBALS["loss_emoji"] = QPixmap(QImage(f"{FOLDER}/Icons/loss.png"))
    GLOBALS["tie_emoji"] = QPixmap(QImage(f"{FOLDER}/Icons/tie.png"))
    window = Window()
    sys.exit(app.exec())

basics.py

from PyQt6.QtCore import Qt
from PyQt6.QtGui import (
    QColor,
    QFocusEvent,
    QFont,
    QFontMetrics,
    QMouseEvent,
    QShowEvent,
)
from PyQt6.QtWidgets import (
    QAbstractButton,
    QCheckBox,
    QColorDialog,
    QComboBox,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QRadioButton,
    QVBoxLayout,
    QWidget,
)
from typing import Iterable
from shared import *

ALIGNMENT = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop


class Font(QFont):
    def __init__(self, size: int = 10) -> None:
        super().__init__()
        self.setFamily("Times New Roman")
        self.setStyleHint(QFont.StyleHint.Times)
        self.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
        self.setPointSize(size)
        self.setBold(True)
        self.setHintingPreference(QFont.HintingPreference.PreferFullHinting)


FONT = Font()
FONT_RULER = QFontMetrics(FONT)


def make_hbox(parent: QWidget = None, margin: int = 3) -> QHBoxLayout:
    return make_box(QHBoxLayout, parent, margin)


def make_vbox(parent: QWidget = None, margin: int = 3) -> QVBoxLayout:
    return make_box(QVBoxLayout, parent, margin)


class CustomLabel(QLabel):
    def __init__(self, text: str, name: str, width: int, height: int) -> None:
        super().__init__()
        self.setFont(FONT)
        self.setText(text)
        self.setFixedSize(width, height)
        self.setObjectName(name)


class CenterLabel(CustomLabel):
    def __init__(self, text: str, name: str, width: int, height: int) -> None:
        super().__init__(text, name, width, height)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)


class Score(CenterLabel):
    def __init__(self, text: str, name: str) -> None:
        super().__init__(text, name, 30, 16)


class LongScore(CenterLabel):
    def __init__(self, text: str, name: str) -> None:
        super().__init__(text, name, 42, 16)


class BlackButton(CenterLabel):
    def __init__(self) -> None:
        super().__init__("Disabled", "Disabled", 72, 20)
        self.setStyleSheet(BLACK)


class CheckBox(QCheckBox):
    def __init__(self, text: str) -> None:
        super().__init__()
        self.setFont(FONT)
        self.setText(text)


class Box(QGroupBox):
    def __init__(self) -> None:
        super().__init__()
        self.setAlignment(ALIGNMENT)
        self.setContentsMargins(0, 0, 0, 0)


class Button(QPushButton):
    def __init__(self, text: str) -> None:
        super().__init__()
        self.setFont(FONT)
        self.setFixedSize(72, 20)
        self.setText(text)


class DummyCheckBox(CheckBox):
    def __init__(self, name: str, checked: bool) -> None:
        super().__init__(name)
        self.setDisabled(True)
        self.setChecked(checked)
        self.setObjectName(name)


class RadioButton(QRadioButton):
    def __init__(self, text: str) -> None:
        super().__init__()
        self.setText(text)
        self.setFont(FONT)
        self.setFixedWidth(FONT_RULER.size(0, text).width() + 30)


class ComboBox(QComboBox):
    def __init__(self, texts: Iterable[str]) -> None:
        super().__init__()
        self.setFont(FONT)
        self.addItems(texts)
        self.setContentsMargins(3, 3, 3, 3)
        self.setFixedWidth(100)


def make_box(
    box_type: type[QHBoxLayout] | type[QVBoxLayout] | type[QGridLayout],
    parent: QWidget,
    margin: int,
) -> QHBoxLayout | QVBoxLayout | QGridLayout:
    box = box_type(parent) if parent else box_type()
    box.setAlignment(ALIGNMENT)
    box.setContentsMargins(*[margin] * 4)
    return box


class Label(QLabel):
    def __init__(self, text: str) -> None:
        super().__init__()
        self.setFont(FONT)
        self.set_text(text)

    def autoResize(self) -> None:
        self.Height = FONT_RULER.size(0, self.text()).height()
        self.Width = FONT_RULER.size(0, self.text()).width()
        self.setFixedSize(self.Width + 3, self.Height + 3)

    def set_text(self, text: str) -> None:
        self.setText(text)
        self.autoResize()


class ColorEdit(QLineEdit):
    def __init__(self, text: str) -> None:
        super().__init__()
        self.setFixedSize(72, 20)
        self.setFont(FONT)
        self.setInputMask("\#HHHHHH")
        self.setText(text)
        self.color = text

    def focusOutEvent(self, e: QFocusEvent) -> None:
        super().focusOutEvent(e)
        if len(self.text()) != 7:
            self.setText(self.color)
        else:
            self.returnPressed.emit()


class ColorPicker(QWidget):
    instances = []

    def __init__(self) -> None:
        super().__init__()
        self.init_window()
        self.add_dialog()
        self.set_style()
        self.dialog.accepted.connect(self.close)
        self.dialog.rejected.connect(self.close)
        ColorPicker.instances.append(self)

    def init_window(self) -> None:
        self.setWindowIcon(GLOBALS["ICON"])
        self.setWindowTitle("Color Picker")
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.MSWindowsFixedSizeDialogHint
        )
        self.vbox = make_vbox(self, 0)
        self.vbox.addWidget(TitleBar("Color Picker", self.close, False))
        self.setFixedSize(518, 436)
        self.frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        self.frame.moveCenter(center)
        self.setObjectName("Picker")

    def add_dialog(self) -> None:
        self.dialog = QColorDialog()
        self.dialog.setWindowFlag(Qt.WindowType.FramelessWindowHint)
        grid = self.dialog.findChild(QGridLayout)
        for i in range(grid.count()):
            grid.itemAt(i).widget().setFont(FONT)

        for e in self.dialog.findChildren(QAbstractButton):
            e.setFont(FONT)
            e.setMinimumWidth(72)
            e.setFixedHeight(20)

        for child in self.dialog.children():
            if isinstance(child, QWidget):
                child.setFont(FONT)

        self.vbox.addWidget(self.dialog)

    def showEvent(self, e: QShowEvent) -> None:
        self.move(self.frame.topLeft())
        self.dialog.show()
        e.accept()

    def set_style(self) -> None:
        self.setStyleSheet(STYLIZER.get_style())


class BasicColorGetter(Box):
    def __init__(self, color: str) -> None:
        super().__init__()
        self.widgets = {}
        self.vbox = make_vbox(self)
        self.set_color(color)

    def init_GUI(self) -> None:
        self.widgets["editor"] = ColorEdit(self.color_text)
        self.widgets["picker"] = ColorPicker()

    def init_connections(self) -> None:
        self.widgets["button"].clicked.connect(self._show_color)
        self.widgets["picker"].dialog.accepted.connect(self.pick_color)
        self.widgets["editor"].returnPressed.connect(self.edit_color)

    def set_color(self, text: str) -> None:
        self.color = [int(text[a:b], 16) for a, b in ((1, 3), (3, 5), (5, 7))]

    @property
    def color_text(self) -> str:
        r, g, b = self.color
        return f"#{r:02x}{g:02x}{b:02x}"

    def _show_color(self) -> None:
        self.widgets["picker"].dialog.setCurrentColor(QColor(*self.color))
        self.widgets["picker"].show()

    def pick_color(self) -> None:
        self.color = self.widgets["picker"].dialog.currentColor().getRgb()[:3]
        self.widgets["editor"].setText(self.color_text)
        self.widgets["editor"].color = self.color_text

    def edit_color(self) -> None:
        text = self.widgets["editor"].text()
        self.widgets["editor"].color = text
        self.set_color(text)
        self.widgets["editor"].clearFocus()


class SquareButton(QPushButton):
    def __init__(self, icon: str) -> None:
        super().__init__()
        self.setIcon(GLOBALS[icon])
        self.setFixedSize(32, 32)


class TitleBar(QGroupBox):
    def __init__(self, name: str, exitfunc: Callable, minimize: bool = True) -> None:
        super().__init__()
        self.hbox = make_hbox(self, 0)
        self.name = name
        self.exitfunc = exitfunc
        self.start = None
        self.setObjectName("Title")
        self.add_icon()
        self.add_title()
        self.add_buttons(minimize)
        self.setFixedHeight(32)

    def add_icon(self) -> None:
        self.icon = QLabel()
        self.icon.setPixmap(GLOBALS["Logo"])
        self.icon.setFixedSize(32, 32)
        self.hbox.addWidget(self.icon)
        self.hbox.addStretch()

    def add_title(self) -> None:
        self.label = Label(self.name)
        self.hbox.addWidget(self.label)
        self.hbox.addStretch()

    def add_buttons(self, minimize: bool = True) -> None:
        if minimize:
            self.minimize_button = SquareButton("Minimize")
            self.minimize_button.clicked.connect(self.minimize)
            self.hbox.addWidget(self.minimize_button)

        self.close_button = SquareButton("Close")
        self.close_button.clicked.connect(self.exitfunc)
        self.hbox.addWidget(self.close_button)

    def mousePressEvent(self, e: QMouseEvent) -> None:
        if e.button() == Qt.MouseButton.LeftButton:
            self.start = e.position()

    def mouseMoveEvent(self, e: QMouseEvent) -> None:
        if self.start is not None:
            self.window().move((e.globalPosition() - self.start).toPoint())

    def mouseReleaseEvent(self, e: QMouseEvent) -> None:
        self.start = None

    def minimize(self) -> None:
        self.window().showMinimized()


class MessageBox(QWidget):
    instances = []

    def __init__(self, title: str, text: str, icon: str, option: str) -> None:
        super().__init__()
        MessageBox.instances.append(self)
        self.setObjectName("Window")
        self.title = title
        self.init_window()
        self.setFixedSize(200, 136)
        self.frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        self.frame.moveCenter(center)
        self.add_widgets(text, icon, option)
        self.set_style()

    def init_window(self) -> None:
        self.setWindowTitle(self.title)
        self.setWindowIcon(GLOBALS["ICON"])
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.MSWindowsFixedSizeDialogHint
        )
        self.vbox = make_vbox(self, 0)
        self.vbox.addWidget(TitleBar(self.title, self.close, False))
        self.hbox = QHBoxLayout()
        self.vbox.addLayout(self.hbox)
        self.hbox.addStretch()
        self.buttonbar = QHBoxLayout()
        self.buttonbar.addStretch()
        self.vbox.addLayout(self.buttonbar)

    def add_widgets(self, text: str, icon: str, option: str) -> None:
        self.icon = CenterLabel("", "", 72, 72)
        self.icon.setPixmap(GLOBALS[icon])
        self.hbox.addWidget(self.icon)
        self.label = Label(text)
        self.label.setObjectName(self.title)
        self.hbox.addWidget(self.label)
        self.hbox.addStretch()
        self.ok = Button(option)
        self.ok.clicked.connect(self.close)
        self.buttonbar.addWidget(self.ok)

    def set_style(self) -> None:
        self.setStyleSheet(STYLIZER.get_style())

    def showEvent(self, e: QShowEvent) -> None:
        self.move(self.frame.topLeft())
        e.accept()

advanced.py

from basics import *
from gamecontrol import Game
from logic import *
from PyQt6.QtCore import QSize, Qt, pyqtSignal
from PyQt6.QtWidgets import QAbstractItemView, QScrollArea, QTableWidget
from shared import *


SQUARESTYLES = {
    name: Style_Compiler(f"QLabel#{name}", CONFIG[config])
    for name, config in zip(CELLNAMES, CELLKEYS)
}
GLOBALS["cellstyles"] = {key: CONFIG[key].copy() for key in CELLKEYS}
CELLSTYLES = {
    name: Style_Compiler(f"QLabel#{name}", GLOBALS["cellstyles"][config])
    for name, config in zip(CELLNAMES, CELLKEYS)
}


PLAYER_SETTINGS_PATH = Path(f"{FOLDER}/config/player_settings.json")
if PLAYER_SETTINGS_PATH.is_file():
    PLAYER_SETTINGS = json.loads(PLAYER_SETTINGS_PATH.read_text())
else:
    PLAYER_SETTINGS = {
        "P1": {
            "Blend": "HSL color",
            "Color": (255, 178, 255),
            "Player": "Human",
            "Shape": "Circle",
        },
        "P2": {
            "Blend": "HSV color",
            "Color": (20, 40, 80),
            "Player": "Master AI",
            "Shape": "Circle",
        },
    }


class ColorGetter(BasicColorGetter):
    def __init__(self, widget: str, key: str, name: str, group: str) -> None:
        self.config = CONFIG[widget] if widget else CONFIG
        self.key = key
        self.name = name
        self.group = group
        super().__init__(self.config[key])
        self.init_GUI()
        self.init_connections()

    def init_GUI(self) -> None:
        super().init_GUI()
        self.widgets["button"] = Button(self.color_text)
        self.vbox.addWidget(Label(self.name))
        self.vbox.addWidget(self.widgets["editor"])
        self.vbox.addWidget(self.widgets["button"])

    def init_connections(self) -> None:
        GLOBALS["RandomizeButton"].change.connect(self.sync_config)
        GLOBALS["RevertButton"].revert.connect(self.sync_config)
        super().init_connections()

    def pick_color(self) -> None:
        GLOBALS["revertcheckboxes"][self.group].setDisabled(False)
        GLOBALS["revertible"] = True
        super().pick_color()
        self._update()

    def edit_color(self) -> None:
        GLOBALS["revertcheckboxes"][self.group].setDisabled(False)
        GLOBALS["revertible"] = True
        super().edit_color()
        self._update()

    def _update(self) -> None:
        self.widgets["button"].setText(self.color_text)
        self.config[self.key] = self.color_text
        GLOBALS["Animation"].change.emit()
        GLOBALS["Preview"].update_style()

    def sync_config(self) -> None:
        color_html = self.config[self.key]
        self.color = [int(color_html[a:b], 16) for a, b in ((1, 3), (3, 5), (5, 7))]
        self.widgets["editor"].setText(color_html)
        self.widgets["editor"].color = color_html
        self.widgets["button"].setText(color_html)


class BorderStylizer(Box):
    def __init__(self, widget: str, key: str, name: str, group: str) -> None:
        super().__init__()
        self.config = CONFIG[widget] if widget else CONFIG
        self.key = key
        self.name = name
        self.borderstyle = self.config[key]
        self.init_GUI()
        self.group = group
        GLOBALS["RandomizeButton"].change.connect(self.sync_config)
        GLOBALS["RevertButton"].revert.connect(self.sync_config)

    def init_GUI(self) -> None:
        self.vbox = make_vbox(self)
        self.vbox.addWidget(Label(self.name))
        self.combobox = ComboBox(BORDER_STYLES)
        self.vbox.addWidget(self.combobox)
        self.combobox.setCurrentText(self.borderstyle)
        self.combobox.currentTextChanged.connect(self._update)

    def _update(self) -> None:
        GLOBALS["revertcheckboxes"][self.group].setDisabled(False)
        GLOBALS["revertible"] = True
        self.borderstyle = self.combobox.currentText()
        self.config[self.key] = self.borderstyle
        GLOBALS["Preview"].update_style()

    def sync_config(self) -> None:
        self.borderstyle = self.config[self.key]
        self.combobox.currentTextChanged.disconnect(self._update)
        self.combobox.setCurrentText(self.borderstyle)
        self.combobox.currentTextChanged.connect(self._update)


class RightPane(QScrollArea):
    def __init__(self, group: str, width: int = 150, height: int = 400) -> None:
        super().__init__()
        self.group = group
        self.box = Box()
        self.vbox = make_vbox(self.box)
        self.setWidget(self.box)
        self.setFixedSize(width, height)
        self.setWidgetResizable(True)
        self.setObjectName("Scroll")
        self.add_widgets()

    def add_widgets(self) -> None:
        for name, key in WIDGET_GROUPS[self.group]:
            self.vbox.addWidget(Label(name))
            for k in CONFIG[key]:
                cls = BorderStylizer if k == "borderstyle" else ColorGetter
                self.vbox.addWidget(cls(key, k, k, self.group))


class PieceButton(QPushButton):
    def __init__(self, name: str) -> None:
        super().__init__()
        self.name = name
        self.setObjectName(name)
        self.setFixedSize(100, 100)
        self.setIconSize(QSize(100, 100))


class PieceColorGetter(BasicColorGetter):
    changed = pyqtSignal()

    def __init__(self, color: str, name: str) -> None:
        super().__init__(color)
        self.init_GUI(name)
        self.init_connections()

    def init_GUI(self, name: str) -> None:
        super().init_GUI()
        self.widgets["button"] = PieceButton(name)
        self.hbox = QHBoxLayout()
        self.hbox.addWidget(self.widgets["editor"])
        self.vbox.addLayout(self.hbox)
        self.vbox.addWidget(self.widgets["button"])

    def edit_color(self) -> None:
        super().edit_color()
        self.changed.emit()

    def pick_color(self) -> None:
        super().pick_color()
        self.changed.emit()


class BaseCell(CenterLabel):
    def __init__(
        self,
        index: int,
        driver: str,
        state: str,
    ) -> None:
        super().__init__("", "", 100, 100)
        self.index = index
        self.checked = False
        self.driver = driver
        self.state = state
        self.init_connections()

    def init_connections(self) -> None:
        GLOBALS[self.driver].clear.connect(self.reset)
        GLOBALS[self.driver].restore.connect(self.reset)

    def change(self) -> None:
        if GLOBALS["animate"]["Board"][0]:
            self._change()

    def reset(self) -> None:
        self.setObjectName("")
        self.clear()
        self.setStyleSheet("")
        self.checked = False


class Square(BaseCell):
    def __init__(self, index: int) -> None:
        super().__init__(index, "Animation", "autostate")
        GLOBALS["Animation"].change.connect(self.change)

    def _change(self) -> None:
        icon, name = GLOBALS[self.state][self.index]
        if icon:
            self.setPixmap(icon)

        if name:
            self.setObjectName(name)
            self.setStyleSheet(SQUARESTYLES[name].compile_style())


class Cell(BaseCell):
    def __init__(self, index: int) -> None:
        super().__init__(index, "Game", "gamestate")
        GLOBALS["Game"].change.connect(self._change)

    def _change(self) -> None:
        piece, name = GLOBALS[self.state][self.index]
        if piece:
            self.setPixmap(piece.active.qpixmap)
            self.checked = True

        if name:
            self.setObjectName(name)
            self.setStyleSheet(CELLSTYLES[name].compile_style())
            self.checked = True


class BaseBoard(QTableWidget):
    def __init__(
        self,
        self_name: str,
        board_name: str,
        cls: type[Cell] | type[Square],
    ) -> None:
        super().__init__()
        GLOBALS[self_name] = self
        self.set_size()
        self.cells = [cls(i) for i in range(9)]
        for i, cell in enumerate(self.cells):
            self.setCellWidget(*divmod(i, 3), cell)

        GLOBALS[board_name] = Game_Board()
        self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)

    def set_size(self) -> None:
        self.setColumnCount(3)
        self.setRowCount(3)
        for i in (0, 1, 2):
            self.setColumnWidth(i, 100)
            self.setRowHeight(i, 100)

        self.horizontalHeader().setVisible(False)
        self.verticalHeader().setVisible(False)
        self.setFixedSize(304, 304)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)


class Board(BaseBoard):
    styler = Style_Compiler("QTableWidget#Game::item:hover", CONFIG["board_hover"])

    def __init__(self) -> None:
        GLOBALS["Game"] = Game()
        super().__init__("GUIBoard", "GameBoard", Cell)
        self.setObjectName("Game")
        GLOBALS["gamestate"] = [[None, None] for _ in range(9)]
        self.cellPressed.connect(self.onClick)
        self.interactive = True
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)

    def set_interactive(self, interactive: bool) -> None:
        name = ""
        style = BOARD_BASE.format_map(CONFIG["board_base"])
        if interactive:
            name = "Game"
            style += self.styler.compile_style()

        self.setObjectName(name)
        self.setStyleSheet(style)
        self.interactive = interactive

    def onClick(self, row: int, col: int) -> None:
        if self.interactive and not (cell := self.cellWidget(row, col)).checked:
            GLOBALS["Game"].chosen = cell.index
            GLOBALS["Game"].human_move()

    def update_images(self) -> None:
        for cell in self.cells:
            if cell.checked:
                cell._change()

    def reset(self) -> None:
        self.clearSelection()
        for cell in self.cells:
            cell.reset()


class BasicPlayerBox(Box):
    def __init__(self, number: int, order: str) -> None:
        super().__init__()
        self.setObjectName(f"P{number + 1}")
        self.number = number
        self.order = order

    def init_GUI(self, cls: type[Score] | type[LongScore]) -> None:
        self.vbox = make_vbox(self)
        hbox = QHBoxLayout()
        hbox.addWidget(Label(f"Player {self.number + 1}"))
        self.vbox.addLayout(hbox)
        self.add_scores(cls)
        self.indicator = Label("")

    def add_scores(self, cls: type[Score] | type[LongScore]) -> None:
        self.scores = {}
        self.grid = QGridLayout()
        for i, label in enumerate(("Win", "Loss", "Tie")):
            self.grid.addWidget(CenterLabel(label, label, 30, 16), 0, i)
            self.grid.addWidget(score := cls("0", label), 1, i)
            self.scores[label] = score

    @property
    def player(self) -> str:
        return GLOBALS[self.order][self.number]


class PlayerBox(BasicPlayerBox):
    def __init__(self, number: int) -> None:
        super().__init__(number, "player_order")
        self.init_GUI()
        self.update_indicator()
        GLOBALS["Animation"].gameover.connect(self.update_scores)
        GLOBALS["Animation"].clear.connect(self.update_scores)
        GLOBALS["Animation"].orderchange.connect(self.update_indicator)

    def init_GUI(self) -> None:
        super().init_GUI(Score)
        self.vbox.addStretch()
        self.vbox.addWidget(self.indicator)
        self.vbox.addStretch()
        self.vbox.addLayout(self.grid)
        self.vbox.addStretch()
        self.setFixedSize(120, 130)

    def update_indicator(self) -> None:
        self.indicator.set_text(f"Current: {GLOBALS[self.player]['name']}")

    def update_scores(self) -> None:
        for k, v in GLOBALS[self.player]["stats"].items():
            self.scores[k].setText(str(v))


class GamePlayerBox(BasicPlayerBox):
    def __init__(self, number: int) -> None:
        super().__init__(number, "game_player_order")
        self.init_GUI()
        self.update_indicator()
        GLOBALS["Game"].gameover.connect(self.update_scores)
        GLOBALS["Game"].clear.connect(self.update_scores)
        GLOBALS["Game"].orderchange.connect(self.update_indicator)

    def init_GUI(self) -> None:
        super().init_GUI(LongScore)
        hbox = QHBoxLayout()
        hbox.addWidget(self.indicator)
        hbox1 = QHBoxLayout()
        hbox1.addLayout(self.grid)
        self.vbox.addLayout(hbox)
        self.vbox.addLayout(hbox1)

    def update_indicator(self) -> None:
        self.indicator.set_text(f"Current: {self.player}")

    def update_scores(self) -> None:
        for k, v in GLOBALS["stats"][self.player].items():
            self.scores[k].setText(str(v))


class BasicStats(Box):
    def __init__(self, control: str, prefix: str) -> None:
        super().__init__()
        self.control = control
        self.prefix = prefix

    def init_GUI(self) -> None:
        self.set_game_count()
        self.set_turn_count()
        self.set_active_player()
        self.set_winner()

    def init_connections(self) -> None:
        animation = GLOBALS[self.control]
        animation.gameover.connect(self.set_game_count)
        animation.gameover.connect(self.set_winner)
        animation.playermove.connect(self.set_active_player)
        animation.change.connect(self.set_turn_count)
        animation.clear.connect(self.set_turn_count)
        animation.clear.connect(self.set_active_player)
        animation.clear.connect(self.set_winner)

    def set_game_count(self) -> None:
        self.quad["game"].set_text(f'Game count: {GLOBALS[f"{self.prefix}game_count"]}')

    def set_turn_count(self) -> None:
        self.quad["turn"].set_text(f'Turn count: {GLOBALS[f"{self.prefix}turn_count"]}')

    def set_active_player(self) -> None:
        self.quad["active"].set_text(f'Active: {GLOBALS[f"{self.prefix}active"]}')

    def set_winner(self) -> None:
        self.quad["winner"].set_text(f'Winner: {GLOBALS[f"{self.prefix}winner"]}')


class StatsBox(BasicStats):
    def __init__(self) -> None:
        super().__init__("Animation", "")
        self._init_GUI()
        self.init_connections()

    def _init_GUI(self) -> None:
        self.setObjectName("Stats")
        self.grid = QGridLayout(self)
        self.quad = {}
        for i, key in enumerate(QUADDRUPLE):
            label = Label("")
            self.quad[key] = label
            self.grid.addWidget(label, *divmod(i, 2))

        self.setFixedHeight(130)
        super().init_GUI()


class StatsBar(BasicStats):
    def __init__(self) -> None:
        super().__init__("Game", "live_")
        self._init_GUI()
        self.init_connections()

    def _init_GUI(self) -> None:
        self.setObjectName("Stats")
        self.hbox = make_hbox(self)
        self.quad = {}
        for i, key in enumerate(QUADDRUPLE):
            label = Label("")
            self.quad[key] = label
            self.hbox.addWidget(label)
            if i != 3:
                self.hbox.addStretch()

        self.setFixedHeight(32)
        super().init_GUI()


class Player(Box):
    def __init__(self, label: str, name: str) -> None:
        super().__init__()
        item = PLAYER_SETTINGS[name]
        GLOBALS[name] = self.piece = Piece(
            item["Color"], item["Blend"], item["Shape"], name
        )
        self.item = item
        self.label = label
        self.changed = False
        self.number = int(name[1]) - 1
        self.name = name
        self.boxname = f"{name}Box"
        GLOBALS[self.boxname] = self
        self.setObjectName(name)
        self.setup_GUI()
        self.set_player_info()

    def set_player_info(self) -> None:
        game = GLOBALS["Game"]
        player = self.item["Player"]
        game.new_players[self.name] = (self.piece, player)
        if player == "Human":
            game.new_match["Human"] = (self.piece, self.name)
        else:
            game.new_match["AI"] = (self.name, player)

        game.new_players["changed"] = True
        game.switch_order()
        game.auto_start()

    def setup_GUI(self) -> None:
        self.vbox = make_vbox(self, 3)
        self.vbox.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        self._setup_playerbox()
        self._setup_player()
        self._setup_shape()
        self._setup_blend()
        self._setup_color()
        self.setFixedSize(150, 330)

    def _setup_playerbox(self) -> None:
        self.playerbox = GamePlayerBox(self.number)
        self.playerbox.setObjectName(self.name)
        self.playerbox.setStyleSheet("QGroupBox { border: 0px;}")
        self.vbox.addWidget(self.playerbox)

    def _setup_player(self) -> None:
        self.player_choice_box = ComboBox(PLAYERS["players"])
        self.player_choice_box.currentTextChanged.connect(self.update_player)
        self.player_choice_box.setCurrentText(self.item["Player"])
        hbox = QHBoxLayout()
        hbox.addWidget(self.player_choice_box)
        self.vbox.addLayout(hbox)

    def _setup_shape(self) -> None:
        self.shape_box = ComboBox(SHAPES)
        self.shape_box.setCurrentText(self.item["Shape"])
        self.shape_box.currentTextChanged.connect(self.pick_piece)
        hbox = QHBoxLayout()
        hbox.addWidget(self.shape_box)
        self.vbox.addLayout(hbox)

    def _setup_blend(self) -> None:
        self.blend_box = ComboBox(BLEND_MODES)
        self.blend_box.setCurrentText(self.item["Blend"])
        self.blend_box.currentTextChanged.connect(self.pick_blend)
        hbox = QHBoxLayout()
        hbox.addWidget(self.blend_box)
        self.vbox.addLayout(hbox)

    def _setup_color(self) -> None:
        r, g, b = self.item["Color"]
        self.colorgetter = PieceColorGetter(f"#{r:02x}{g:02x}{b:02x}", self.name)
        self.colorgetter.vbox.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        self.colorgetter.widgets["button"].setIcon(self.piece.active.qicon)
        self.colorgetter.setObjectName(self.name)
        self.colorgetter.setStyleSheet("QGroupBox { border: 0px;}")
        self.vbox.addWidget(self.colorgetter)
        self.colorgetter.changed.connect(self.set_color)

    def pick_piece(self) -> None:
        self.item["Shape"] = shape = self.shape_box.currentText()
        self.piece.set_active(shape)
        self.update_icons()

    def set_color(self) -> None:
        self.item["Color"] = self.piece.color = self.colorgetter.color
        self.piece.set_color()
        self.update_icons()

    def pick_blend(self) -> None:
        self.item["Blend"] = blend = self.blend_box.currentText()
        self.piece.set_blend(blend)
        self.update_icons()

    def update_icons(self) -> None:
        self.piece.set_icon()
        self.colorgetter.widgets["button"].setIcon(self.piece.active.qicon)
        GLOBALS["GUIBoard"].update_images()

    def update_player(self) -> None:
        self.item["Player"] = self.player_choice_box.currentText()
        self.changed = True
        self.set_player_info()
        GLOBALS["Window"].check_pairing()

preview.py

import ctypes
import sys
from advanced import *
from animation import *
from basics import *
from control import *
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QCloseEvent
from shared import *
from theme import *


class DummyGroupBox(QGroupBox):
    def __init__(self, text: str, name: str) -> None:
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.hbox = QHBoxLayout(self)
        self.hbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setObjectName(name)
        self.hbox.addWidget(Label(text))
        self.setFixedSize(120, 130)


class Customizer(Box):
    def __init__(self, mx: int, my: int, w: int, h: int = 420) -> None:
        super().__init__()
        self.hbox = make_hbox(self, mx)
        self.vbox = make_vbox(None, my)
        self.hbox.addLayout(self.vbox)
        self.setFixedSize(w, h)


class ComboBox_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 280)
        self.init_GUI()

    def init_GUI(self) -> None:
        self.vbox.addWidget(UniBox())
        self.vbox.addWidget(ListBox())
        self.hbox.addWidget(RightPane("ComboBox"))


class Button_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 250)
        self.init_GUI()

    def init_GUI(self) -> None:
        for i, button in enumerate(BUTTONS):
            self.vbox.addWidget(DummyButton(button, i))

        self.vbox.addWidget(BlackButton())
        self.hbox.addWidget(RightPane("Button"))


class LineEdit_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 250)
        self.init_GUI()

    def init_GUI(self) -> None:
        for i in range(15):
            self.vbox.addWidget(DummyEdit(i))

        self.hbox.addWidget(RightPane("LineEdit"))


class SpinBox_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 0, 230)
        self.init_GUI()

    def init_GUI(self) -> None:
        for i, n in enumerate(GLOBALS[24]):
            self.vbox.addWidget(SpinBox(n, i))

        self.hbox.addWidget(RightPane("SpinBox"))


class CheckBox_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 0, 240)
        self.init_GUI()

    def init_GUI(self) -> None:
        for i, (name, state) in enumerate(CHECKBOXES):
            self.vbox.addWidget(AnimationCheckBox(name, state, i))

        self.vbox.addWidget(DummyCheckBox("Disabled", False))
        self.hbox.addWidget(RightPane("CheckBox"))


class Misc_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 420)
        self.init_GUI()

    def init_GUI(self) -> None:
        self.grid = QGridLayout()
        self.grid.addWidget(PlayerBox(0), 0, 0)
        self.grid.addWidget(PlayerBox(1), 0, 1)
        self.grid.addWidget(StatsBox(), 1, 0, 1, 2)
        self.grid.addWidget(DummyGroupBox("GroupBox", ""), 2, 0)
        self.grid.addWidget(DummyGroupBox("Background", "Window"), 2, 1)
        self.vbox.addLayout(self.grid)
        self.hbox.addWidget(RightPane("Miscellaneous"))


class RadioButton_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 260)
        self.init_GUI()

    def init_GUI(self) -> None:
        for i, (name, state) in enumerate(RADIOBUTTONS):
            self.vbox.addWidget(DummyRadioButton(name, state, i))
            self.vbox.addStretch()

        radio = RadioButton("Disabled")
        radio.setDisabled(True)
        self.vbox.addWidget(radio)
        self.hbox.addWidget(RightPane("RadioButton"))


class Board_Customizer(Customizer):
    def __init__(self) -> None:
        super().__init__(3, 3, 470)
        self.init_GUI()

    def init_GUI(self) -> None:
        self.vbox.addWidget(AutoBoard())
        row1 = QHBoxLayout()
        row2 = QHBoxLayout()
        for name in SEXTET:
            row1.addWidget(CenterLabel(name, "", 45, 20))
            row2.addWidget(CenterLabel("", name, 42, 42))

        self.vbox.addLayout(row1)
        self.vbox.addLayout(row2)
        row3 = QHBoxLayout()
        row3.addWidget(Label("Player 1:"))
        row3.addWidget(PlayerRadioButton("Cross", True, "PX"))
        row3.addWidget(PlayerRadioButton("Nought", False, "PO"))
        self.vbox.addLayout(row3)

        self.hbox.addWidget(RightPane("Board"))


FIRST_ROW = (
    Board_Customizer,
    Misc_Customizer,
    CheckBox_Customizer,
    lambda: RightPane("ScrollArea", 150, 420),
)
SECOND_ROW = (
    ComboBox_Customizer,
    RadioButton_Customizer,
    Button_Customizer,
    LineEdit_Customizer,
    SpinBox_Customizer,
)


CINQLIGNES = (
    (QHBoxLayout, "addLayout", True),
    (QHBoxLayout, "addLayout", True),
    (Randomizer, "addWidget", False),
    (Reverter, "addWidget", False),
    (QHBoxLayout, "addLayout", True),
)


class Preview(QWidget):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowIcon(GLOBALS["ICON"])
        self.setWindowTitle("Preview")
        self.setFixedSize(1304, 1004)
        GLOBALS["Preview"] = self
        self.qthread = Animation(self)
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.MSWindowsFixedSizeDialogHint
        )
        self.init_icons()
        self.add_rows()
        self.init_GUI()

    def add_rows(self) -> None:
        self.box = make_vbox(self, 0)
        self.box.addWidget(TitleBar("Preview", stop_and_exit))
        self.vbox = make_vbox()
        self.box.addLayout(self.vbox)
        self.rows = []
        for cls, func, is_row in CINQLIGNES:
            row = cls()
            getattr(self.vbox, func)(row)
            if is_row:
                self.rows.append(row)

    def init_GUI(self) -> None:
        self.rows[2].addWidget(AnimationBar())
        self.rows[2].addWidget(ControlBox())
        for cls in FIRST_ROW:
            self.rows[0].addWidget(cls())

        for cls in SECOND_ROW:
            self.rows[1].addWidget(cls())

        self.setObjectName("Window")
        self.update_style()
        self.frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        self.frame.moveCenter(center)

    def update_style(self) -> None:
        self.setStyleSheet(STYLIZER.get_style())

    def closeEvent(self, e: QCloseEvent) -> None:
        stop_and_exit()

    def showEvent(self, e: QShowEvent) -> None:
        self.move(self.frame.topLeft())
        e.accept()

    @staticmethod
    def init_icons() -> None:
        GLOBALS.update(
            {
                "X": QPixmap(QImage(X, 100, 100, QImage.Format.Format_RGBA8888)),
                "O": QPixmap(QImage(O, 100, 100, QImage.Format.Format_RGBA8888)),
            }
        )


def stop_and_exit() -> None:
    GLOBALS["run"] = False
    QTest.qWait(125)
    GLOBALS["Preview"].close()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")
    GLOBALS["ICON"] = QIcon(f"{FOLDER}/Icons/logo.png")
    GLOBALS["Logo"] = QPixmap(QImage(f"{FOLDER}/Icons/logo.png")).scaled(
        32, 32, Qt.AspectRatioMode.KeepAspectRatio
    )
    GLOBALS["Close"] = QIcon(f"{FOLDER}/Icons/close.png")
    GLOBALS["Minimize"] = QIcon(f"{FOLDER}/Icons/minimize.png")
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("Tic Tac Toe")
    window = Preview()
    window.show()
    sys.exit(app.exec())

basic_data.py

import blend_modes
import json
from itertools import cycle
from logic import STATES_P1, STATES_P2_COUNTER
from pathlib import Path


SWITCH = cycle(["Stop", "Run"])
FOLDER = str(Path(__file__).parent).replace("\\", "/")
CONFIG_PATH = Path(f"{FOLDER}/config/theme.json")
CONFIG = json.loads(CONFIG_PATH.read_text())
DEFAULT_CONFIG = json.loads(Path(f"{FOLDER}/config/default_theme.json").read_text())
WIDGETS = json.loads(Path(f"{FOLDER}/config/widgets.json").read_text())
WIDGET_GROUPS = json.loads(Path(f"{FOLDER}/config/widget_groups.json").read_text())
SECRET = json.loads(Path(f"{FOLDER}/data/secret.json").read_text())
SECRET_TOO = json.loads(Path(f"{FOLDER}/data/secret_too.json").read_text())

BOX = ("background", "bordercolor", "borderstyle")
GRADIENT = (
    "qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 {lowlight}, stop: 0.75 {highlight}, stop: 1 {lowlight})",
    "qlineargradient(x1: 0, y1: 0, x2: 0.75, y2: 0, x3: 1, y3: 0, stop: 0 {lowlight}, stop: 0.75 {highlight}, stop: 1 {lowlight})",
)

NORMAL_KEYS = {
    "background",
    "bordercolor",
    "borderstyle",
    "highlight",
    "lowlight",
    "textcolor",
}

EXTRA_ATTRIBUTES = {
    "QLabel#Mock": {"margin-left": "1px"},
    "QLabel#Top": {"margin-left": "1px"},
    "DummyButton#Base": {
        "border-style": "outset",
        "border-width": "3px",
        "border-radius": "6px",
    },
    "DummyButton#Hover": {
        "border-style": "outset",
        "border-width": "3px",
        "border-radius": "6px",
    },
    "DummyButton#Pressed": {
        "border-style": "inset",
        "border-width": "3px",
        "border-radius": "6px",
    },
    "TitleBar": {"border": "0px"},
    "QTableWidget#Game::item:hover": {"margin": "4px"},
    "QScrollBar::vertical": {"width": "16px"},
    "QScrollBar::handle:vertical": {"min-height": "80px", "margin": "18px"},
    "QRadioButton#Base::indicator, QRadioButton#Hover::indicator, QRadioButton#Pressed::indicator, QRadioButton::indicator": {
        "width": "16px",
        "height": "16px",
        "border-radius": "10px",
    },
}

MOCK_MENU_BASE = """QGroupBox#Menu {{
    background: {background};
    border: 3px {borderstyle} {bordercolor};
}}
"""


BOARD_BASE = """QTableWidget {{
    background: {background};
}}
QTableWidget::item {{
    border: 3px {borderstyle} {bordercolor};
}}
"""

HOVER_LABEL_BASE = """QLabel#High, QLabel#Focus {{
    padding-left: 1px;
    margin-right: 6px;
    background: {hoverbase};
    color: {hovercolor};
}}
QLabel#High {{
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
}}
"""

COMBOBOX_BASE = """QComboBox {{
    border-radius: 6px;
    background: {background_1};
    border: 3px {borderstyle_1} {bordercolor_1};
    color: {textcolor};
    selection-background-color: {hoverbase};
    selection-color: {hovercolor};
}}
QComboBox QAbstractItemView {{
    border-radius: 6px;
    background: {background_2};
    border: 3px {borderstyle_2} {bordercolor_2};
}}
"""

SHAPES = (
    "Circle",
    "Triangle 0",
    "Triangle 1",
    "Triangle 2",
    "Triangle 3",
    "Square",
    "Diamond",
    "Pentagon 0",
    "Pentagon 1",
    "Pentagon 2",
    "Pentagon 3",
    "Hexagon 0",
    "Hexagon 1",
    "Ring",
    "Rhombus 0",
    "Rhombus 1",
    "Cross 0",
    "Cross 1",
    "Pentagram 0",
    "Pentagram 1",
    "Pentagram 2",
    "Pentagram 3",
    "Hexagram 0",
    "Hexagram 1",
)

BLEND_MODES = {
    "Lighten": blend_modes.blend_lighten,
    "Screen": blend_modes.blend_screen,
    "Color dodge": blend_modes.blend_color_dodge,
    "Linear dodge": blend_modes.blend_linear_dodge,
    "Darken": blend_modes.blend_darken,
    "Multiply": blend_modes.blend_multiply,
    "Color burn": blend_modes.blend_color_burn,
    "Linear burn": blend_modes.blend_linear_burn,
    "Overlay": blend_modes.blend_overlay,
    "Soft light": blend_modes.blend_soft_light,
    "Hard light": blend_modes.blend_hard_light,
    "Vivid light": blend_modes.blend_vivid_light,
    "Linear light": blend_modes.blend_linear_light,
    "Pin light": blend_modes.blend_pin_light,
    "Reflect": blend_modes.blend_reflect,
    "Difference": blend_modes.blend_difference,
    "Exclusion": blend_modes.blend_exclusion,
    "Subtract": blend_modes.blend_subtract,
    "Grain extract": blend_modes.blend_grain_extract,
    "Grain merge": blend_modes.blend_grain_merge,
    "Divide": blend_modes.blend_divide,
    "HSV color": blend_modes.blend_HSV_Color,
    "HSL color": blend_modes.blend_HSL_Color,
    "Color lux": blend_modes.blend_color_lux,
    "Color nox": blend_modes.blend_color_nox,
    "LCh D65": blend_modes.blend_color_LCh_D65,
    "LCh D50": blend_modes.blend_color_LCh_D50,
}


IMMUTABLE = """DummyButton#Base, DummyButton#Hover, QPushButton, QPushButton#P1, QPushButton#P2, SquareButton {{
    border-style: outset;
    border-width: 3px;
    border-radius: 6px;
}}
DummyButton#Pressed, QPushButton:pressed, QPushButton#P1:pressed, QPushButton#P2:pressed {{
    border-style: inset;
    border-width: 3px;
    border-radius: 6px;
}}
QPushButton:disabled {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #000000, stop: 0.75 #808080, stop: 1 #000000);
    color: #ffffff;
    border: 3px outset #202020;
}}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
    border: 0px;
    width: 10px;
    height: 10px;
}}
QSpinBox::up-button {{
    border-width: 0px;
}}
QSpinBox::down-button {{
    border-width: 0px;
}}
QSpinBox::down-arrow:hover, QSpinBox::up-arrow:hover {{
    background: #0000ff;
}}
QSpinBox::down-arrow:pressed, QSpinBox::up-arrow:pressed {{
    background: #404080;
}}
QComboBox::drop-down {{
    border: 0px;
    padding: 0px 0px 0px 0px;
}}
QCheckBox::indicator {{
    border: 0px;
    width: 16px;
    height: 16px;
}}
QCheckBox::indicator:unchecked, QCheckBox#Base::indicator:unchecked, QCheckBox#Hover::indicator:unchecked, QCheckBox#Pressed::indicator:unchecked  {{
    image: url({folder}/icons/checkbox-unchecked.png);
}}
QCheckBox::indicator:checked, QCheckBox#Base::indicator:checked, QCheckBox#Hover::indicator:checked, QCheckBox#Pressed::indicator:checked {{
    image: url({folder}/icons/checkbox-checked.png);
}}
QCheckBox::indicator:disabled {{
    image: url({folder}/icons/checkbox-disabled.png);
}}
QRadioButton::indicator:checked, QRadioButton#Base::indicator:checked, QRadioButton#Hover::indicator:checked, QRadioButton#Pressed::indicator:checked {{
    image: url({folder}/icons/radiobutton-checked.png)
}}
QRadioButton::indicator:unchecked, QRadioButton#Base::indicator:unchecked, QRadioButton#Hover::indicator:unchecked, QRadioButton#Pressed::indicator:unchecked {{
    image: url({folder}/icons/radiobutton-unchecked.png)
}}
QRadioButton::indicator:disabled {{
    image: url({folder}/icons/radiobutton-disabled.png)
}}
QComboBox::down-arrow {{
    image: url({folder}/icons/combobox-downarrow.png);
    width: 10px;
    height: 10px;
}}
QScrollBar::up-arrow:vertical {{
    image: url("{folder}/icons/scrollbar-uparrow.png")
}}
QScrollBar::down-arrow:vertical {{
    image: url("{folder}/icons/scrollbar-downarrow.png")
}}
QSpinBox::up-arrow, QSpinBox#Dummy::up-arrow {{
    image: url({folder}/icons/spinbox-uparrow.png);
}}
QSpinBox::down-arrow, QSpinBox#Dummy::down-arrow {{
    image: url({folder}/icons/spinbox-downarrow.png);
}}
QSpinBox::up-arrow:disabled, QSpinBox::up-arrow:off {{
    image: url({folder}/icons/spinbox-uparrow-disabled.png);
}}
QSpinBox::down-arrow:disabled, QSpinBox::down-arrow:off {{
    image: url({folder}/icons/spinbox-downarrow-disabled.png);
}}
""".format(
    folder=FOLDER
)


GLOBALS = {
    "pause": False,
    "run": False,
    "rungame": False,
    "popup": False,
    24: SECRET[0].copy(),
    15: SECRET_TOO[0].copy(),
    "24": cycle(range(24)),
    "15": cycle(range(15)),
    "PX": {
        "states": STATES_P1,
        "id": "P1",
        "name": "Master AI",
        "opponent": "PO",
        "stats": {"Win": 0, "Loss": 0, "Tie": 0},
    },
    "PO": {
        "states": STATES_P2_COUNTER,
        "id": "P2",
        "name": "Super AI",
        "opponent": "PX",
        "stats": {"Win": 0, "Loss": 0, "Tie": 0},
    },
    "game_count": 0,
    "turn_count": 0,
    "active": "Master AI",
    "winner": "null",
    "live_active": "Human",
    "live_game_count": 0,
    "live_turn_count": 0,
    "live_winner": "null",
    "order": ("cross", "nought"),
    "player_order": ["PX", "PO"],
    "game_player_order": ["Human", "Master AI"],
    "new_order": ["PX", "PO"],
    "groups": {
        "Board": [True, True],
        "Button": [True, True],
        "CheckBox": [True, True],
        "ComboBox": [True, True],
        "LineEdit": [True, True],
        "Miscellaneous": [True, True],
        "RadioButton": [True, True],
        "ScrollArea": [True, True],
        "SpinBox": [True, True],
    },
    "revertcheckboxes": {},
    "animate": {
        "Board": [True, "animate_game"],
        "Button": [True, "animate_buttons"],
        "CheckBox": [True, "animate_checkbox"],
        "ComboBox": [True, "animate_combobox"],
        "LineEdit": [True, "animate_lineedit"],
        "RadioButton": [True, "animate_radiobuttons"],
        "SpinBox": [True, "animate_spinbox"],
    },
    "revertible": False,
    "orderchanged": False,
}

shared.py

import numpy as np
from basic_data import *
from copy import deepcopy
from itertools import cycle
from logic import STATES_P1, STATES_P2, STATES_P1_COUNTER, STATES_P2_COUNTER
from theme import *
from PIL import Image

BLACK = """QLabel#Disabled {
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #000000, stop: 0.75 #808080, stop: 1 #000000);
    color: #ffffff;
    border: 3px outset #202020;
    border-radius: 6px;
}"""

X = bytearray(np.array(Image.open(f"{FOLDER}/icons/X.png")))
O = bytearray(np.array(Image.open(f"{FOLDER}/icons/O.png")))
RUN = cycle(["Stop", "Start"])
PAUSE = cycle(["Resume", "Pause"])
STATES = (STATES_P1, STATES_P2)
COUNTER_STATES = (STATES_P1_COUNTER, STATES_P2_COUNTER)
SEXDECIM = ["High"] + ["Mock"] * 15
SEXTET = ("Cell", "Hover", "P1", "P2", "P1Win", "P2Win")
NONSENSE = [
    "Lorem",
    "ipsum",
    "dolor",
    "sit",
    "amet",
    "consectetur",
    "adipiscing",
    "elit",
    "sed",
    "do",
    "eiusmod",
    "tempor",
    "incididunt",
    "ut",
    "labore",
    "et",
]

PLAYERINFO = {
    "PX": ["PO", "cross", STATES_P1, STATES_P2],
    "PO": ["PX", "nought", STATES_P1_COUNTER, STATES_P2_COUNTER],
}
CONTROL = (
    ("Default", "restore_default"),
    ("OK", "apply_style"),
    ("Cancel", "revert_style"),
)
QUADDRUPLE = (
    "game",
    "turn",
    "active",
    "winner",
)
STYLIZER = Style_Combiner(CONFIG)
NONSENSE_COPY = NONSENSE.copy()
TRINITY = ["Base", "Hover", "Pressed"]
BUTTONS = TRINITY * 5
BUTTONS_COPY = BUTTONS.copy()
CHECKBOX_STATES = [(t, i) for i in (0, 1) for t in TRINITY]
CHECKBOXES = CHECKBOX_STATES * 3
CHECKBOXES_COPY = CHECKBOXES.copy()
RADIOBUTTONS = CHECKBOX_STATES * 2
RADIOBUTTONS_COPY = RADIOBUTTONS.copy()
CONFIG_COPY = deepcopy(CONFIG)


PLAYER_NAMES = (
    "Human",
    "Novice AI",
    "Adept AI",
    "Master AI",
    "Master AI+",
    "Super AI",
    "Super AI+",
)

PLAYERS = {
    "players": PLAYER_NAMES,
    "opponents": {
        k: PLAYER_NAMES[:i] + PLAYER_NAMES[i + 1 :] for i, k in enumerate(PLAYER_NAMES)
    },
}

BORDER_STYLES = (
    "dotted",
    "dashed",
    "solid",
    "double",
    "groove",
    "ridge",
    "inset",
    "outset",
    "none",
    "hidden",
)

CELLNAMES = ("Cell", "Hover", "P1", "P2", "P1Win", "P2Win")
CELLKEYS = (
    "board_base",
    "board_hover",
    "player1_base",
    "player2_base",
    "player1_win",
    "player2_win",
)

The above scripts are responsible for creating the GUI windows, but you need the files that create the QThreads that control the UI to make the the program run, the threads are automatically created and linked to by the widgets, and you also need the configuration files, the pictures and the blending modes script and color space script and game logic files, you can find those files in the linked repository, and I will also post them later.

I hope the GUI is self explanatory so I won't bother to describe what each component of the UI does. The project is extremely complicated, and I made several mistakes, during testing the program frequently crashes, and the crash is caused by yet another bug. I spent well over two months on this project, and according to the time stamps, I started the project at most as late as 2023-08-28, possibly earlier.

I wrote all lines of code all by myself, and I also created the pictures used in the project. I have fixed all bugs I can find and implemented everything I originally planned.

This is the first time I have done something this complex, and I want to know, is my coding style Pythonic? Is the code well organized and structured? Are there any bugs that I have missed? And are there other improvements that can be made in general?


Update

It has been two days since the questions were posted, and I haven't received a response yet, so it is time to bump them.

I have made a few changes. First I have fixed a small bug.

Then I made some redesigns of the UI, as you can see, inside playerboxes the widgets are now positioned in the center, and I have made the score labels wide enough so that they can show 6 digit numbers.

And obviously, I have added the ability to customize the title bar. I have also made every window completely unresizable, and I added emojis to the game over messages.

And last but not least, I have made the random styles half-decent, I have generated hue, saturation and lightness values separately, the hues have a minimum value of 180 and a maximum value of 330 and they are biased to 249.33, if the target is a background color, else it can be any hue.

The saturation values have a lower bound of 0.5 and upper bound of 1 (1 being full saturation), and they are biased towards 0.7284.

And HSV values of the colors are determined by the role of the color keys, each role has a lower bound and an upper bound and target value.

And then the HSV color is converted to RGB color, this ensures the generated styles don't look downright awful to me, but I don't know others' preferences.

And now the program cannot randomly choose "dashed", "dotted", "hidden", "none" border styles, and the 6 remaining border styles have different chances to be chosen as well.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ This is huge, might take me like days to review but seems like a cool project. \$\endgroup\$
    – The_AH
    Commented Oct 31, 2023 at 8:55
  • \$\begingroup\$ So the bounty will have to be wasted then. \$\endgroup\$ Commented Nov 6, 2023 at 15:18

0

Browse other questions tagged or ask your own question.