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 = [