diff --git a/.pylintrc b/.pylintrc index 5e7888d..d31ba5f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -31,7 +31,7 @@ extension-pkg-allow-list= # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. (This is an alternative name to extension-pkg-allow-list # for backward compatibility.) -extension-pkg-whitelist= +extension-pkg-whitelist=PyQt6 # Return non-zero exit code if any of these messages/categories are detected, # even if score is above --fail-under value. Syntax same as enable. Messages diff --git a/clashroyalebuildabot/bot/bot.py b/clashroyalebuildabot/bot/bot.py index 709aad1..5eedbca 100644 --- a/clashroyalebuildabot/bot/bot.py +++ b/clashroyalebuildabot/bot/bot.py @@ -1,16 +1,12 @@ -import os import random -import sys import threading import time import keyboard from loguru import logger -import yaml from clashroyalebuildabot.constants import ALL_TILES from clashroyalebuildabot.constants import ALLY_TILES -from clashroyalebuildabot.constants import DEBUG_DIR from clashroyalebuildabot.constants import DISPLAY_CARD_DELTA_X from clashroyalebuildabot.constants import DISPLAY_CARD_HEIGHT from clashroyalebuildabot.constants import DISPLAY_CARD_INIT_X @@ -19,7 +15,6 @@ from clashroyalebuildabot.constants import DISPLAY_HEIGHT from clashroyalebuildabot.constants import LEFT_PRINCESS_TILES from clashroyalebuildabot.constants import RIGHT_PRINCESS_TILES -from clashroyalebuildabot.constants import SRC_DIR from clashroyalebuildabot.constants import TILE_HEIGHT from clashroyalebuildabot.constants import TILE_INIT_X from clashroyalebuildabot.constants import TILE_INIT_Y @@ -39,22 +34,17 @@ class Bot: is_paused_logged = False is_resumed_logged = True - def __init__(self, actions, auto_start=True): + def __init__(self, actions, config): self.actions = actions - self.auto_start = auto_start + self.auto_start = config["bot"]["auto_start_game"] self.end_of_game_clicked = False - - self._setup_logger() + self.should_run = True cards = [action.CARD for action in actions] if len(cards) != 8: raise ValueError(f"Must provide 8 cards but was given: {cards}") self.cards_to_actions = dict(zip(cards, actions)) - config_path = os.path.join(SRC_DIR, "config.yaml") - with open(config_path, encoding="utf-8") as file: - config = yaml.safe_load(file) - self.visualizer = Visualizer(**config["visuals"]) self.emulator = Emulator(**config["adb"]) self.detector = Detector(cards=cards) @@ -78,34 +68,24 @@ def _log_and_wait(prefix, delay): logger.info(message) time.sleep(delay) - @staticmethod - def _setup_logger(): - config_path = os.path.join(SRC_DIR, "config.yaml") - with open(config_path, encoding="utf-8") as file: - config = yaml.safe_load(file) - log_level = config.get("bot", {}).get("log_level", "INFO").upper() - logger.remove() - logger.add(sys.stdout, level=log_level) - logger.add( - os.path.join(DEBUG_DIR, "bot.log"), - rotation="500 MB", - level=log_level, - ) - @staticmethod def _handle_keyboard_shortcut(): while True: keyboard.wait("ctrl+p") - if pause_event.is_set(): - logger.info("Bot paused.") - pause_event.clear() - Bot.is_paused_logged = True - Bot.is_resumed_logged = False - else: - logger.info("Bot resumed.") - pause_event.set() - Bot.is_resumed_logged = True - Bot.is_paused_logged = False + Bot.pause_or_resume() + + @staticmethod + def pause_or_resume(): + if pause_event.is_set(): + logger.info("Bot paused.") + pause_event.clear() + Bot.is_paused_logged = True + Bot.is_resumed_logged = False + else: + logger.info("Bot resumed.") + pause_event.set() + Bot.is_resumed_logged = True + Bot.is_paused_logged = False @staticmethod def _get_nearest_tile(x, y): @@ -232,11 +212,15 @@ def step(self): def run(self): try: - while True: + while self.should_run: if not pause_event.is_set(): time.sleep(0.1) continue self.step() + logger.info("Thanks for using CRBAB, see you next time!") except KeyboardInterrupt: logger.info("Thanks for using CRBAB, see you next time!") + + def stop(self): + self.should_run = False diff --git a/clashroyalebuildabot/config.yaml b/clashroyalebuildabot/config.yaml index 12dafee..d050fbf 100644 --- a/clashroyalebuildabot/config.yaml +++ b/clashroyalebuildabot/config.yaml @@ -5,7 +5,9 @@ bot: log_level: "DEBUG" # Create a deck code when the game starts - load_deck: True + load_deck: False + auto_start_game: False + enable_gui: True adb: # The IP address of your device or emulator. diff --git a/clashroyalebuildabot/gui/animations.py b/clashroyalebuildabot/gui/animations.py new file mode 100644 index 0000000..074cdee --- /dev/null +++ b/clashroyalebuildabot/gui/animations.py @@ -0,0 +1,29 @@ +from PyQt6.QtCore import QEasingCurve +from PyQt6.QtCore import QPropertyAnimation +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QGraphicsDropShadowEffect + + +def start_play_button_animation(self): + self.glow_effect = QGraphicsDropShadowEffect(self) + self.glow_effect.setBlurRadius( + 10 + ) # Initial blur radius for the glow effect + self.glow_effect.setColor(Qt.GlobalColor.cyan) + self.glow_effect.setOffset( + 0, 0 + ) # Center the shadow to create the glow effect + self.start_stop_button.setGraphicsEffect(self.glow_effect) + + _start_glow_animation(self) + + +def _start_glow_animation(self): + """Create a glow effect animation.""" + self.glow_animation = QPropertyAnimation(self.glow_effect, b"blurRadius") + self.glow_animation.setStartValue(0) + self.glow_animation.setEndValue(25) + self.glow_animation.setDuration(2000) + self.glow_animation.setEasingCurve(QEasingCurve.Type.SineCurve) + self.glow_animation.setLoopCount(-1) + self.glow_animation.start() diff --git a/clashroyalebuildabot/gui/gameplay_widget.py b/clashroyalebuildabot/gui/gameplay_widget.py new file mode 100644 index 0000000..2e283d7 --- /dev/null +++ b/clashroyalebuildabot/gui/gameplay_widget.py @@ -0,0 +1,45 @@ +from PyQt6.QtGui import QImage +from PyQt6.QtGui import QPixmap +from PyQt6.QtWidgets import QLabel +from PyQt6.QtWidgets import QVBoxLayout +from PyQt6.QtWidgets import QWidget + + +class ImageStreamWindow(QWidget): + def __init__(self): + super().__init__() + + self.image = QLabel(self) + self.inactiveIndicator = QLabel(self) + self.inactiveIndicator.setText( + "The visualizer is disabled. Enable it in the Settings tab." + ) + self.inactiveIndicator.setStyleSheet( + "background-color: #FFA500; color: white; padding: 5px; height: fit-content; width: fit-content;" + ) + self.inactiveIndicator.setMaximumHeight(30) + layout = QVBoxLayout() + layout.addWidget(self.inactiveIndicator) + layout.addWidget(self.image) + self.setLayout(layout) + + def update_frame(self, annotated_image): + height, width, channel = annotated_image.shape + bytes_per_line = 3 * width + q_image = QImage( + annotated_image.data.tobytes(), + width, + height, + bytes_per_line, + QImage.Format.Format_RGB888, + ) + + pixmap = QPixmap.fromImage(q_image) + self.image.setPixmap(pixmap) + + def update_active_state(self, active): + if not active: + self.inactiveIndicator.show() + else: + self.inactiveIndicator.hide() + self.image.clear() diff --git a/clashroyalebuildabot/gui/layout_setup.py b/clashroyalebuildabot/gui/layout_setup.py new file mode 100644 index 0000000..b62b21f --- /dev/null +++ b/clashroyalebuildabot/gui/layout_setup.py @@ -0,0 +1,254 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont +from PyQt6.QtGui import QPixmap +from PyQt6.QtWidgets import QCheckBox +from PyQt6.QtWidgets import QComboBox +from PyQt6.QtWidgets import QDoubleSpinBox +from PyQt6.QtWidgets import QFormLayout +from PyQt6.QtWidgets import QFrame +from PyQt6.QtWidgets import QGridLayout +from PyQt6.QtWidgets import QGroupBox +from PyQt6.QtWidgets import QHBoxLayout +from PyQt6.QtWidgets import QLabel +from PyQt6.QtWidgets import QLineEdit +from PyQt6.QtWidgets import QPushButton +from PyQt6.QtWidgets import QTabWidget +from PyQt6.QtWidgets import QTextEdit +from PyQt6.QtWidgets import QVBoxLayout +from PyQt6.QtWidgets import QWidget + +from clashroyalebuildabot.gui.gameplay_widget import ImageStreamWindow +from clashroyalebuildabot.gui.utils import save_config + + +def setup_top_bar(main_window): + top_bar = QFrame() + top_bar.setStyleSheet("background-color: #1E272E;") + top_bar_layout = QHBoxLayout(top_bar) + + logo_text_layout = QHBoxLayout() + + logo_label = QLabel() + logo_pixmap = QPixmap("logo.png").scaled( + 120, + 120, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + logo_label.setPixmap(logo_pixmap) + logo_label.setFixedSize(120, 120) + logo_text_layout.addWidget(logo_label) + + text_layout = QVBoxLayout() + text_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + server_name = QLabel("Clash Royale Build-A-Bot") + server_name.setStyleSheet( + "font-weight: bold; font-size: 16pt; color: white;" + ) + text_layout.addWidget(server_name) + + server_details = QLabel( + 'https://github.com/Pbatch/ClashRoyaleBuildABot' + ) + server_details.setOpenExternalLinks(True) + server_details.setStyleSheet("color: #57A6FF;") + text_layout.addWidget(server_details) + + main_window.server_id_label = QLabel("Status: Stopped") + main_window.server_id_label.setStyleSheet("color: #999;") + text_layout.addWidget(main_window.server_id_label) + + logo_text_layout.addLayout(text_layout) + + top_bar_layout.addLayout(logo_text_layout) + + right_layout = QVBoxLayout() + right_layout.setAlignment(Qt.AlignmentFlag.AlignRight) + + button_layout = QHBoxLayout() + button_layout.setAlignment(Qt.AlignmentFlag.AlignRight) + + main_window.play_pause_button = QPushButton("⏸️") + main_window.play_pause_button.setFont(QFont("Arial", 18)) + main_window.play_pause_button.setStyleSheet( + """ + QPushButton { + background-color: #4B6EAF; + color: white; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; + border-radius: 5px; + } + QPushButton:hover { + background-color: #5C7EBF; + } + """ + ) + main_window.play_pause_button.clicked.connect( + main_window.toggle_pause_resume_and_display + ) + + button_layout.addWidget(main_window.play_pause_button) + main_window.play_pause_button.hide() + + main_window.start_stop_button = QPushButton("▶") + main_window.start_stop_button.setFont(QFont("Arial", 18)) + main_window.start_stop_button.setStyleSheet( + """ + QPushButton { + background-color: #4B6EAF; + color: white; + min-width: 32px; + max-width: 32px; + min-height: 32px; + max-height: 32px; + border-radius: 5px; + } + QPushButton:hover { + background-color: #5C7EBF; + } + """ + ) + main_window.start_stop_button.clicked.connect( + main_window.toggle_start_stop + ) + + button_layout.addWidget(main_window.start_stop_button) + + top_bar_layout.addStretch() + top_bar_layout.addLayout(right_layout) + top_bar_layout.addLayout(button_layout) + + return top_bar + + +def setup_tabs(main_window): + tab_widget = QTabWidget() + tab_widget.setStyleSheet( + """ + QTabWidget::pane { border: 0; } + QTabBar::tab { + background: #333; + color: white; + padding: 8px; + margin-bottom: -1px; + } + QTabBar::tab:selected { + background: #1E272E; + border-bottom: 2px solid #57A6FF; + } + """ + ) + + logs_tab = QWidget() + logs_layout = QVBoxLayout(logs_tab) + main_window.log_display = QTextEdit() + main_window.log_display.setReadOnly(True) + main_window.log_display.setStyleSheet( + "background-color: #1e1e1e; color: lightgrey; font-family: monospace;" + ) + logs_layout.addWidget(main_window.log_display) + tab_widget.addTab(logs_tab, "Logs") + + main_window.visualize_tab = ImageStreamWindow() + main_window.visualize_tab.update_active_state( + main_window.config["visuals"]["show_images"] + ) + tab_widget.addTab(main_window.visualize_tab, "Visualize") + + settings_tab = QWidget() + settings_layout = QGridLayout(settings_tab) + + bot_group = QGroupBox("Bot ") + bot_layout = QFormLayout() + main_window.log_level_dropdown = QComboBox() + main_window.log_level_dropdown.addItems( + ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"] + ) + main_window.log_level_dropdown.setCurrentText( + main_window.config["bot"]["log_level"] + ) + bot_layout.addRow("Log Level:", main_window.log_level_dropdown) + + main_window.adb_ip_input = QLineEdit() + main_window.adb_ip_input.setText(main_window.config["adb"]["ip"]) + main_window.device_serial_input = QLineEdit() + main_window.device_serial_input.setText( + main_window.config["adb"]["device_serial"] + ) + bot_layout.addRow("ADB IP Address:", main_window.adb_ip_input) + bot_layout.addRow("Device Serial:", main_window.device_serial_input) + + bot_group.setLayout(bot_layout) + + visuals_group = QGroupBox("Visuals Settings") + visuals_layout = QFormLayout() + main_window.save_labels_checkbox = QCheckBox( + "Save labels to /debug folder" + ) + main_window.save_labels_checkbox.setChecked( + main_window.config["visuals"]["save_labels"] + ) + main_window.save_images_checkbox = QCheckBox( + "Save labeled images to /debug folder" + ) + main_window.save_images_checkbox.setChecked( + main_window.config["visuals"]["save_images"] + ) + main_window.show_images_checkbox = QCheckBox("Enable visualizer") + main_window.show_images_checkbox.setChecked( + main_window.config["visuals"]["show_images"] + ) + visuals_layout.addRow(main_window.save_labels_checkbox) + visuals_layout.addRow(main_window.save_images_checkbox) + visuals_layout.addRow(main_window.show_images_checkbox) + visuals_group.setLayout(visuals_layout) + + save_config_group = QGroupBox() + save_config_layout = QHBoxLayout() + save_config_button = QPushButton("Save config to file") + save_config_button.clicked.connect( + lambda: save_config(main_window.update_config()) + ) + save_config_layout.addWidget(save_config_button) + save_config_group.setLayout(save_config_layout) + save_config_group.setMaximumHeight(50) + + ingame_group = QGroupBox("Ingame Settings") + ingame_layout = QFormLayout() + + main_window.play_action_delay_input = QDoubleSpinBox() + main_window.play_action_delay_input.setRange(0.1, 5.0) + main_window.play_action_delay_input.setSingleStep(0.1) + main_window.play_action_delay_input.setValue( + main_window.config["ingame"]["play_action"] + ) + ingame_layout.addRow( + "Action Delay (sec):", main_window.play_action_delay_input + ) + + main_window.load_deck_checkbox = QCheckBox("Load deck on startup") + main_window.load_deck_checkbox.setChecked( + main_window.config["bot"]["load_deck"] + ) + ingame_layout.addRow(main_window.load_deck_checkbox) + + main_window.auto_start_game_checkbox = QCheckBox("Auto start games") + main_window.auto_start_game_checkbox.setChecked( + main_window.config["bot"]["auto_start_game"] + ) + ingame_layout.addRow(main_window.auto_start_game_checkbox) + + ingame_group.setLayout(ingame_layout) + + settings_layout.addWidget(bot_group, 0, 0) + settings_layout.addWidget(visuals_group, 1, 0) + settings_layout.addWidget(save_config_group, 2, 0) + settings_layout.addWidget(ingame_group, 0, 1, 3, 1) + + tab_widget.addTab(settings_tab, "Settings") + + return tab_widget diff --git a/clashroyalebuildabot/gui/log_handler.py b/clashroyalebuildabot/gui/log_handler.py new file mode 100644 index 0000000..5b83d20 --- /dev/null +++ b/clashroyalebuildabot/gui/log_handler.py @@ -0,0 +1,20 @@ +import logging + +from PyQt6.QtCore import Q_ARG +from PyQt6.QtCore import QMetaObject +from PyQt6.QtCore import Qt + + +class QTextEditLogger(logging.Handler): + def __init__(self, text_edit): + super().__init__() + self.text_edit = text_edit + + def emit(self, record): + log_entry = self.format(record) + QMetaObject.invokeMethod( + self.text_edit, + "append", + Qt.ConnectionType.QueuedConnection, + Q_ARG(str, log_entry), + ) diff --git a/clashroyalebuildabot/gui/main_window.py b/clashroyalebuildabot/gui/main_window.py new file mode 100644 index 0000000..1fd59ea --- /dev/null +++ b/clashroyalebuildabot/gui/main_window.py @@ -0,0 +1,149 @@ +from threading import Thread + +from loguru import logger +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon +from PyQt6.QtGui import QPixmap +from PyQt6.QtWidgets import QApplication +from PyQt6.QtWidgets import QMainWindow +from PyQt6.QtWidgets import QVBoxLayout +from PyQt6.QtWidgets import QWidget + +from clashroyalebuildabot import Bot +from clashroyalebuildabot.bot.bot import pause_event +from clashroyalebuildabot.gui.animations import start_play_button_animation +from clashroyalebuildabot.gui.layout_setup import setup_tabs +from clashroyalebuildabot.gui.layout_setup import setup_top_bar +from clashroyalebuildabot.gui.styles import set_styles +from clashroyalebuildabot.utils.logger import colorize_log +from clashroyalebuildabot.utils.logger import setup_logger + + +class MainWindow(QMainWindow): + def __init__(self, config, actions): + super().__init__() + self.config = config + self.actions = actions + self.bot = None + self.bot_thread = None + self.is_running = False + + self.setWindowTitle(" ") + self.setGeometry(100, 100, 900, 600) + + transparent_pixmap = QPixmap(1, 1) + transparent_pixmap.fill(Qt.GlobalColor.transparent) + self.setWindowIcon(QIcon(transparent_pixmap)) + + main_widget = QWidget(self) + self.setCentralWidget(main_widget) + main_layout = QVBoxLayout(main_widget) + + top_bar = setup_top_bar(self) + tab_widget = setup_tabs(self) + + main_layout.addWidget(top_bar) + main_layout.addWidget(tab_widget) + + set_styles(self) + start_play_button_animation(self) + + def log_handler_function(self, message): + formatted_message = colorize_log(message) + self.log_display.append(formatted_message) + QApplication.processEvents() + self.log_display.verticalScrollBar().setValue( + self.log_display.verticalScrollBar().maximum() + ) + + def toggle_start_stop(self): + if self.is_running: + self.stop_bot() + self.glow_animation.start() + else: + self.start_bot() + self.glow_animation.stop() + + def toggle_pause_resume_and_display(self): + if not self.bot: + return + if pause_event.is_set(): + self.play_pause_button.setText("▶") + else: + self.play_pause_button.setText("⏸️") + self.bot.pause_or_resume() + + def start_bot(self): + if self.is_running: + return + self.update_config() + self.is_running = True + self.bot_thread = Thread(target=self.bot_task) + self.bot_thread.daemon = True + self.bot_thread.start() + self.start_stop_button.setText("■") + self.play_pause_button.show() + self.server_id_label.setText("Status - Running") + logger.info("Starting bot") + + def stop_bot(self): + if self.bot: + self.bot.stop() + self.is_running = False + self.start_stop_button.setText("▶") + self.play_pause_button.hide() + self.server_id_label.setText("Status - Stopped") + logger.info("Bot stopped") + + def restart_bot(self): + if self.is_running: + self.stop_bot() + self.update_config() + self.start_bot() + + def update_config(self) -> dict: + self.config["visuals"][ + "save_labels" + ] = self.save_labels_checkbox.isChecked() + self.config["visuals"][ + "save_images" + ] = self.save_images_checkbox.isChecked() + self.config["visuals"][ + "show_images" + ] = self.show_images_checkbox.isChecked() + self.visualize_tab.update_active_state( + self.config["visuals"]["show_images"] + ) + self.config["bot"]["load_deck"] = self.load_deck_checkbox.isChecked() + self.config["bot"][ + "auto_start_game" + ] = self.auto_start_game_checkbox.isChecked() + log_level_changed = ( + self.config["bot"]["log_level"] + != self.log_level_dropdown.currentText() + ) + self.config["bot"]["log_level"] = self.log_level_dropdown.currentText() + if log_level_changed: + setup_logger(self, self.config) + self.config["ingame"]["play_action"] = round( + float(self.play_action_delay_input.value()), 2 + ) + self.config["adb"]["ip"] = self.adb_ip_input.text() + self.config["adb"]["device_serial"] = self.device_serial_input.text() + return self.config + + def bot_task(self): + try: + self.bot = Bot(actions=self.actions, config=self.config) + self.bot.visualizer.frame_ready.connect( + self.visualize_tab.update_frame + ) + self.bot.run() + self.stop_bot() + except Exception as e: + logger.error(f"Bot crashed: {e}") + self.stop_bot() + raise + + def append_log(self, message): + self.log_display.append(message) diff --git a/clashroyalebuildabot/gui/styles.py b/clashroyalebuildabot/gui/styles.py new file mode 100644 index 0000000..443dac8 --- /dev/null +++ b/clashroyalebuildabot/gui/styles.py @@ -0,0 +1,28 @@ +def set_styles(window): + window.setStyleSheet( + """ + QMainWindow { + background-color: #0D1117; + } + QLabel { + color: white; + padding: 2px; + } + QPushButton { + border: none; + padding: 8px; + } + QFrame { + background-color: #1E272E; + } + QGroupBox { + color: white; + } + QCheckBox { + color: white; + } + QPushButton { + color: white; + } + """ + ) diff --git a/clashroyalebuildabot/gui/utils.py b/clashroyalebuildabot/gui/utils.py new file mode 100644 index 0000000..0d57e68 --- /dev/null +++ b/clashroyalebuildabot/gui/utils.py @@ -0,0 +1,26 @@ +import os + +from loguru import logger +from ruamel.yaml import YAML + +from clashroyalebuildabot.constants import SRC_DIR + +yaml = YAML() + + +def load_config(): + try: + config_path = os.path.join(SRC_DIR, "config.yaml") + with open(config_path, encoding="utf-8") as file: + return yaml.load(file) + except Exception as e: + logger.error(f"Can't parse config, stacktrace: {e}") + + +def save_config(config): + try: + config_path = os.path.join(SRC_DIR, "config.yaml") + with open(config_path, "w", encoding="utf-8") as file: + yaml.dump(config, file) + except Exception as e: + logger.error(f"Can't save config, stacktrace: {e}") diff --git a/clashroyalebuildabot/utils/logger.py b/clashroyalebuildabot/utils/logger.py new file mode 100644 index 0000000..5ce4442 --- /dev/null +++ b/clashroyalebuildabot/utils/logger.py @@ -0,0 +1,48 @@ +import os +import sys + +from loguru import logger + +from clashroyalebuildabot.constants import DEBUG_DIR + +COLORS = dict(context_info="#118aa2", time="#459028") + + +def setup_logger(main_window, config: dict): + log_level = config.get("bot", {}).get("log_level", "INFO").upper() + logger.remove() + logger.add(sys.stdout, level=log_level) + logger.add( + os.path.join(DEBUG_DIR, "bot.log"), + rotation="500 MB", + level=log_level, + ) + logger.add( + main_window.log_handler_function, + format="{time} {level} {module}:{function}:{line} - {message}", + level=log_level, + ) + + +def colorize_log(message): + log_record = message.record + level = log_record["level"].name + time = log_record["time"].strftime("%Y-%m-%d %H:%M:%S") + module = log_record["module"] + function = log_record["function"] + line = log_record["line"] + log_message = log_record["message"] + + if level == "DEBUG": + color = "#147eb8" + elif level == "INFO": + color = "white" + level = "INFO " + elif level == "WARNING": + color = "orange" + elif level == "ERROR": + color = "red" + else: + color = "black" + + return f"""[{time}] {level} | {module}:{function}:{line} - {log_message}""" diff --git a/clashroyalebuildabot/visualizer.py b/clashroyalebuildabot/visualizer.py index 056dc37..1952fb7 100644 --- a/clashroyalebuildabot/visualizer.py +++ b/clashroyalebuildabot/visualizer.py @@ -1,11 +1,11 @@ from dataclasses import asdict import os -import cv2 -from loguru import logger import numpy as np from PIL import ImageDraw from PIL import ImageFont +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtCore import QObject from clashroyalebuildabot.constants import CARD_CONFIG from clashroyalebuildabot.constants import LABELS_DIR @@ -14,7 +14,7 @@ from clashroyalebuildabot.namespaces.units import NAME2UNIT -class Visualizer: +class Visualizer(QObject): _COLOUR_AND_RGBA = [ ["navy", (0, 38, 63, 127)], ["blue", (0, 120, 210, 127)], @@ -33,7 +33,10 @@ class Visualizer: ["silver", (220, 220, 220, 127)], ] + frame_ready = pyqtSignal(np.ndarray) + def __init__(self, save_labels, save_images, show_images): + super().__init__() self.save_labels = save_labels self.save_images = save_images self.show_images = show_images @@ -44,10 +47,6 @@ def __init__(self, save_labels, save_images, show_images): os.makedirs(LABELS_DIR, exist_ok=True) os.makedirs(SCREENSHOTS_DIR, exist_ok=True) - if self.show_images: - cv2.namedWindow("Visualizer", cv2.WINDOW_NORMAL) - logger.info("Visualizer initialized") - @staticmethod def _write_label(image, state, basename): labels = [] @@ -122,5 +121,4 @@ def run(self, image, state): ) if self.show_images: - cv2.imshow("Visualizer", np.array(annotated_image)[..., ::-1]) - cv2.waitKey(1) + self.frame_ready.emit(np.array(annotated_image)) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..2eaaa84 Binary files /dev/null and b/logo.png differ diff --git a/main.py b/main.py index 8d38fe4..125b286 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,6 @@ from clashroyalebuildabot.actions import MusketeerAction from clashroyalebuildabot.actions import WitchAction from clashroyalebuildabot.bot import Bot -from clashroyalebuildabot.utils.git_utils import check_and_pull_updates start_time = datetime.now() @@ -40,7 +39,6 @@ def update_terminal_title(): def main(): - check_and_pull_updates() actions = [ ArchersAction, GoblinBarrelAction, diff --git a/pyproject.toml b/pyproject.toml index 271b4ac..f5a0fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ dependencies = [ "flake8==7.0.0", "isort==5.13.2", "pylint==3.1.0", + "ruamel.yaml>=0.18.6", + "PyQt6>=6.7.1" ] [project.optional-dependencies] cpu = [