import sys import random import math import os from datetime import datetime from collections import deque from typing import List, Tuple, Dict, Any from PyQt6 import QtWidgets, QtCore, QtGui from PyQt6.QtCore import QThread, pyqtSignal, QTimer, QDateTime, Qt, QTime from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QPushButton, QMessageBox, QGroupBox, QSpinBox, QDoubleSpinBox, QCheckBox, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget, QTimeEdit, QFrame, QScrollArea, QSizePolicy ) import matplotlib matplotlib.use('QtAgg') from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure # --- APP_CONFIG --- APP_CONFIG = { 'APP_NAME': "Crash", 'APP_NAME_VERSION': "(v1.0)", 'STATS_FILE_NAME': "stats.txt", 'COLOR_BACKGROUND': "#1e1e2d", 'COLOR_SURFACE': "#2d2d44", 'COLOR_SURFACE_LIGHT': "#3d3d54", 'COLOR_TEXT': "#e0e0ff", 'COLOR_BORDER': "#444466", 'COLOR_PRIMARY': "#bb86fc", 'COLOR_SECONDARY': "#03dac6", 'COLOR_POSITIVE': "#00cc66", 'COLOR_NEGATIVE': "#ff5555", 'COLOR_WARNING': "#ffaa00", 'COLOR_INFO': "#66b3ff", 'TEXT_LABELS': { 'app_title': "Crash", 'save_settings': "Сохранить настройки", 'reset_settings': "Сбросить настройки", 'min_bet_label': "Минимальная ставка:", 'max_bet_label': "Максимальная ставка:", }, } # --- Crash Number Generator --- def generate_crash_number(): """Генерирует случайное число краша""" # Генерируем равномерно распределённое число U ∈ [0, 1) U = random.uniform(0, 1) # Вычисляем множитель по формуле if U >= 0.99999: # Защита от деления на ноль crash_multiplier = 100000.00 else: crash_multiplier = 1.0 / (1.0 - U) # Ограничиваем множитель диапазоном [1.00, 100000.00] и округляем до 2 знаков crash_multiplier = max(min(crash_multiplier, 100000.00), 1.00) return round(crash_multiplier, 2) # --- Crash History Table (для отображения последних 100 значений) --- class CrashHistoryTable(QWidget): def __init__(self, parent=None): super().__init__(parent) self.history = [] self.init_ui() def init_ui(self): self.layout = QVBoxLayout() self.setLayout(self.layout) # Заголовок header_layout = QHBoxLayout() header_label = QLabel("История последних значений краша") header_label.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-size: 12pt; font-weight: bold;") header_label.setToolTip("Последние 100 значений коэффициента краша") header_layout.addWidget(header_label) header_layout.addStretch() self.layout.addLayout(header_layout) # Таблица self.table = QTableWidget() self.table.setColumnCount(2) self.table.setHorizontalHeaderLabels(["#", "Краш"]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table.verticalHeader().setVisible(False) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setSelectionMode(QTableWidget.SelectionMode.NoSelection) self.table.setStyleSheet(f""" QTableWidget {{ font-size: 9pt; color: {APP_CONFIG['COLOR_TEXT']}; background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; gridline-color: {APP_CONFIG['COLOR_BORDER']}; alternate-background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; }} QTableWidget::item {{ padding: 4px; border-bottom: 1px solid {APP_CONFIG['COLOR_BORDER']}; }} QHeaderView::section {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: {APP_CONFIG['COLOR_TEXT']}; padding: 6px; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; font-weight: bold; }} QTableWidget::item:selected {{ background-color: {APP_CONFIG['COLOR_PRIMARY']}; color: white; }} """) self.table.setToolTip("Таблица с последними 100 значениями краша") self.layout.addWidget(self.table) def update_history(self, crash_history: List[float]): """Обновляет таблицу всей историей крашей""" self.history = crash_history[-100:] if len(crash_history) > 100 else crash_history self.table.setRowCount(len(self.history)) for i, value in enumerate(reversed(self.history)): # Номер num_item = QTableWidgetItem(str(len(self.history) - i)) num_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(i, 0, num_item) # Значение краша crash_item = QTableWidgetItem(f"{value:.2f}x") crash_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # Цвет в зависимости от значения if value >= 10: crash_item.setForeground(QtGui.QColor(APP_CONFIG['COLOR_POSITIVE'])) crash_item.setFont(QtGui.QFont("Arial", 9, QtGui.QFont.Weight.Bold)) elif value >= 2: crash_item.setForeground(QtGui.QColor(APP_CONFIG['COLOR_SECONDARY'])) else: crash_item.setForeground(QtGui.QColor(APP_CONFIG['COLOR_NEGATIVE'])) self.table.setItem(i, 1, crash_item) # Выравнивание текста по центру for i in range(self.table.rowCount()): for j in range(self.table.columnCount()): item = self.table.item(i, j) if item: item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # --- Actions History Table (для отображения последних 100 действий) --- class ActionsHistoryTable(QWidget): def __init__(self, parent=None): super().__init__(parent) self.history = [] self.init_ui() def init_ui(self): self.layout = QVBoxLayout() self.setLayout(self.layout) # Заголовок header_layout = QHBoxLayout() header_label = QLabel("История последних действий") header_label.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-size: 12pt; font-weight: bold;") header_label.setToolTip("Последние 100 записей о действиях в тестировании") header_layout.addWidget(header_label) header_layout.addStretch() self.layout.addLayout(header_layout) # Таблица self.table = QTableWidget() self.table.setColumnCount(10) self.table.setHorizontalHeaderLabels([ "Дата/Время", "Раунд", "Банк", "Ставка", "Множитель", "Краш", "Результат", "Новый банк", "Профит", "ROI" ]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table.verticalHeader().setVisible(False) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setSelectionMode(QTableWidget.SelectionMode.NoSelection) self.table.setStyleSheet(f""" QTableWidget {{ font-size: 8pt; color: {APP_CONFIG['COLOR_TEXT']}; background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; gridline-color: {APP_CONFIG['COLOR_BORDER']}; alternate-background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; }} QTableWidget::item {{ padding: 2px; border-bottom: 1px solid {APP_CONFIG['COLOR_BORDER']}; }} QHeaderView::section {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: {APP_CONFIG['COLOR_TEXT']}; padding: 4px; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; font-weight: bold; }} QTableWidget::item:selected {{ background-color: {APP_CONFIG['COLOR_PRIMARY']}; color: white; }} """) self.table.setToolTip("Таблица с последними 100 записями о действиях в тестировании") self.layout.addWidget(self.table) def update_history(self, actions_history: List[Dict[str, Any]]): """Обновляет таблицу всей историей действий""" self.history = actions_history[-100:] if len(actions_history) > 100 else actions_history self.table.setRowCount(len(self.history)) for i, data in enumerate(reversed(self.history)): timestamp = data['timestamp'].strftime("%H:%M:%S.%f")[:-3] # Обрезаем до миллисекунд self.table.setItem(i, 0, QTableWidgetItem(timestamp)) self.table.setItem(i, 1, QTableWidgetItem(str(data['round_num']))) self.table.setItem(i, 2, QTableWidgetItem(f"{data['balance_before']:.2f}")) self.table.setItem(i, 3, QTableWidgetItem(str(data['bet']))) self.table.setItem(i, 4, QTableWidgetItem(f"{data['multiplier']:.2f}")) self.table.setItem(i, 5, QTableWidgetItem(f"{data['crash_value']:.2f}")) # Результат if data['win']: result_text = f"+{data['reward']:.2f}" result_item = QTableWidgetItem(result_text) result_item.setForeground(QtGui.QColor(APP_CONFIG['COLOR_POSITIVE'])) result_item.setFont(QtGui.QFont("Arial", 8, QtGui.QFont.Weight.Bold)) else: result_text = f"-{data['bet']:.2f}" result_item = QTableWidgetItem(result_text) result_item.setForeground(QtGui.QColor(APP_CONFIG['COLOR_NEGATIVE'])) result_item.setFont(QtGui.QFont("Arial", 8, QtGui.QFont.Weight.Bold)) self.table.setItem(i, 6, result_item) self.table.setItem(i, 7, QTableWidgetItem(f"{data['balance_after']:.2f}")) self.table.setItem(i, 8, QTableWidgetItem(f"{data['profit']:.2f}")) self.table.setItem(i, 9, QTableWidgetItem(f"{data['roi']:.2f}%")) # Выравнивание текста по центру for i in range(self.table.rowCount()): for j in range(self.table.columnCount()): item = self.table.item(i, j) if item: item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # --- Testing Thread --- class TestingWorker(QThread): # Сигналы для обновления GUI batch_finished = pyqtSignal(dict) # Передаем данные батча testing_finished = pyqtSignal(dict) # итоговая статистика testing_stopped = pyqtSignal() error_occurred = pyqtSignal(str) plot_data_ready = pyqtSignal(list, list) # данные для графика def __init__(self, num_rounds: int, min_bet: int, max_bet: int, min_multiplier: float, max_multiplier: float, initial_balance: float, update_interval: int): super().__init__() self.num_rounds = num_rounds self.min_bet = min_bet self.max_bet = max_bet self.min_multiplier = min_multiplier self.max_multiplier = max_multiplier self.initial_balance = initial_balance self.update_interval = update_interval # 0 = только в конце self._is_running = True self.balance = initial_balance self.crash_history = [] self.actions_history = [] self.testing_rounds_data = [] self.testing_profit_data = [] self.total_wins = 0 self.total_losses = 0 self.current_win_streak = 0 self.current_loss_streak = 0 self.max_win_streak = 0 self.max_loss_streak = 0 self.total_invested = 0 # Для правильного расчета ROI self.processed_rounds = 0 def stop(self): self._is_running = False def run(self): try: batch_data = { 'rounds': [], 'crash_values': [], 'bets': [], 'multipliers': [], 'wins': [], 'rewards': [], 'balances_before': [], 'balances_after': [], 'profits': [], 'rois': [], 'winrates': [], 'timestamps': [], 'total_wins_list': [], 'total_losses_list': [], 'max_win_streaks': [], 'max_loss_streaks': [] } for i in range(1, self.num_rounds + 1): if not self._is_running: self.testing_stopped.emit() return # 1. Генерация краша crash_value = generate_crash_number() self.crash_history.append(crash_value) # 2. Генерация случайной ставки и множителя bet = random.randint(self.min_bet, self.max_bet) multiplier = round(random.uniform(self.min_multiplier, self.max_multiplier), 2) # 3. Симуляция результата balance_before = self.balance win = crash_value >= multiplier reward = 0 if bet > 0: self.total_invested += bet # Увеличиваем общую сумму ставок self.balance -= bet if win: win_amount = bet * multiplier self.balance += win_amount reward = win_amount - bet # Чистый выигрыш self.total_wins += 1 self.current_win_streak += 1 self.current_loss_streak = 0 if self.current_win_streak > self.max_win_streak: self.max_win_streak = self.current_win_streak else: reward = -bet # Потеря ставки self.total_losses += 1 self.current_loss_streak += 1 self.current_win_streak = 0 if self.current_loss_streak > self.max_loss_streak: self.max_loss_streak = self.current_loss_streak profit = self.balance - self.initial_balance # Исправленный расчет ROI roi = (profit / self.total_invested * 100) if self.total_invested > 0 else 0 winrate = (self.total_wins / i * 100) if i > 0 else 0 # Winrate от общего количества раундов # 4. Сохранение данных для истории действий action_data = { 'round_num': i, 'crash_value': crash_value, 'bet': bet, 'multiplier': multiplier, 'win': win, 'reward': reward, 'balance_before': balance_before, 'balance_after': self.balance, 'profit': profit, 'roi': roi, 'winrate': winrate, 'timestamp': datetime.now(), 'total_wins': self.total_wins, 'total_losses': self.total_losses, 'max_win_streak': self.max_win_streak, 'max_loss_streak': self.max_loss_streak } self.actions_history.append(action_data) # 5. Обновление данных для графика self.testing_rounds_data.append(i) self.testing_profit_data.append(profit) # 6. Подготовка данных для батча (если нужно обновление) if self.update_interval > 0: batch_data['rounds'].append(i) batch_data['crash_values'].append(crash_value) batch_data['bets'].append(bet) batch_data['multipliers'].append(multiplier) batch_data['wins'].append(win) batch_data['rewards'].append(reward) batch_data['balances_before'].append(balance_before) batch_data['balances_after'].append(self.balance) batch_data['profits'].append(profit) batch_data['rois'].append(roi) batch_data['winrates'].append(winrate) batch_data['timestamps'].append(datetime.now()) batch_data['total_wins_list'].append(self.total_wins) batch_data['total_losses_list'].append(self.total_losses) batch_data['max_win_streaks'].append(self.max_win_streak) batch_data['max_loss_streaks'].append(self.max_loss_streak) self.processed_rounds += 1 # 7. Эмит сигналов при достижении интервала обновления if self.update_interval > 0 and self.processed_rounds % self.update_interval == 0: batch_stats = { 'processed_rounds': self.processed_rounds, 'total_rounds': self.num_rounds, 'current_balance': self.balance, 'current_profit': profit, 'current_roi': roi, 'current_winrate': winrate, 'total_wins': self.total_wins, 'total_losses': self.total_losses, 'max_win_streak': self.max_win_streak, 'max_loss_streak': self.max_loss_streak, 'crash_history': self.crash_history.copy(), 'actions_history': self.actions_history.copy(), 'plot_rounds': self.testing_rounds_data.copy(), 'plot_profits': self.testing_profit_data.copy() } self.batch_finished.emit(batch_stats) # Очищаем батч данные batch_data = { 'rounds': [], 'crash_values': [], 'bets': [], 'multipliers': [], 'wins': [], 'rewards': [], 'balances_before': [], 'balances_after': [], 'profits': [], 'rois': [], 'winrates': [], 'timestamps': [], 'total_wins_list': [], 'total_losses_list': [], 'max_win_streaks': [], 'max_loss_streaks': [] } # Небольшая задержка только для очень маленьких интервалов if self.update_interval > 0 and self.update_interval < 10: self.msleep(1) # Тестирование завершено final_stats = { 'total_rounds': self.num_rounds, 'final_balance': self.balance, 'total_profit': self.balance - self.initial_balance, 'roi': ((self.balance - self.initial_balance) / self.total_invested * 100) if self.total_invested > 0 else 0, 'winrate': (self.total_wins / self.num_rounds * 100) if self.num_rounds > 0 else 0, 'total_wins': self.total_wins, 'total_losses': self.total_losses, 'max_win_streak': self.max_win_streak, 'max_loss_streak': self.max_loss_streak, 'crash_history': self.crash_history, 'actions_history': self.actions_history, 'plot_rounds': self.testing_rounds_data, 'plot_profits': self.testing_profit_data } self.testing_finished.emit(final_stats) except Exception as e: self.error_occurred.emit(str(e)) # --- Main Window --- class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(f"{APP_CONFIG['APP_NAME']} {APP_CONFIG['APP_NAME_VERSION']}") self.setGeometry(100, 100, 1400, 900) self.setStyleSheet(f""" QMainWindow {{ background-color: {APP_CONFIG['COLOR_BACKGROUND']}; }} QWidget {{ background-color: {APP_CONFIG['COLOR_BACKGROUND']}; color: {APP_CONFIG['COLOR_TEXT']}; font-family: 'Segoe UI', Arial, sans-serif; }} QLabel {{ color: {APP_CONFIG['COLOR_TEXT']}; background-color: transparent; }} QGroupBox {{ border: 2px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 8px; margin-top: 10px; padding: 10px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; background-color: {APP_CONFIG['COLOR_SURFACE']}; }} QGroupBox::title {{ subline-offset: -15px; padding: 0px 5px 0px 5px; color: {APP_CONFIG['COLOR_SECONDARY']}; }} QPushButton {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; padding: 8px 15px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; min-width: 100px; }} QPushButton:hover {{ background-color: {APP_CONFIG['COLOR_PRIMARY']}; border: 1px solid {APP_CONFIG['COLOR_SECONDARY']}; }} QPushButton:pressed {{ background-color: {APP_CONFIG['COLOR_SECONDARY']}; }} QPushButton:disabled {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: #888888; border: 1px solid #555555; }} QSpinBox, QDoubleSpinBox {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; color: {APP_CONFIG['COLOR_TEXT']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 4px; padding: 4px; selection-background-color: {APP_CONFIG['COLOR_PRIMARY']}; }} QSpinBox::up-button, QDoubleSpinBox::up-button {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; border: none; width: 16px; }} QSpinBox::down-button, QDoubleSpinBox::down-button {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; border: none; width: 16px; }} QCheckBox {{ color: {APP_CONFIG['COLOR_TEXT']}; }} QTabWidget::pane {{ border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; background-color: {APP_CONFIG['COLOR_BACKGROUND']}; }} QTabBar::tab {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; color: {APP_CONFIG['COLOR_TEXT']}; padding: 8px 15px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; }} QTabBar::tab:selected {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: {APP_CONFIG['COLOR_SECONDARY']}; font-weight: bold; }} QTabBar::tab:hover {{ background-color: {APP_CONFIG['COLOR_PRIMARY']}; }} QHeaderView::section {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: {APP_CONFIG['COLOR_TEXT']}; padding: 6px; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; font-weight: bold; }} """) # Инициализация UI self.init_ui() def init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) self.main_layout = QVBoxLayout(central_widget) self.main_layout.setSpacing(10) self.setup_main_tabs() def setup_main_tabs(self): self.tabs = QTabWidget() self.tab_main = self.create_main_tab_interface() self.tab_settings = self.create_settings_tab_interface() self.tab_testing = self.create_testing_tab_interface() self.tab_history = self.create_history_tables_tab_interface() # Новая вкладка self.tabs.addTab(self.tab_main, "Главная") self.tabs.addTab(self.tab_settings, "Настройки") self.tabs.addTab(self.tab_testing, "Тестирование") self.tabs.addTab(self.tab_history, "Таблицы (История)") # Добавляем новую вкладку self.main_layout.addWidget(self.tabs) def create_main_tab_interface(self): tab = QWidget() layout = QVBoxLayout() layout.setSpacing(15) tab.setLayout(layout) # Вкладка теперь полностью пустая, как требовалось layout.addStretch() return tab def create_settings_tab_interface(self): tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(15) # Заголовок header_label = QLabel("Настройки тестирования") header_label.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-size: 16pt; font-weight: bold; padding: 10px;") header_label.setAlignment(Qt.AlignmentFlag.AlignCenter) header_label.setToolTip("Настройка параметров для симуляции игры Crash") layout.addWidget(header_label) # Основные настройки main_settings_group = QGroupBox("Основные параметры") main_settings_layout = QFormLayout() main_settings_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) main_settings_layout.setHorizontalSpacing(20) main_settings_layout.setVerticalSpacing(10) main_settings_group.setToolTip("Основные параметры симуляции") # Начальный банк self.initial_balance_input = QSpinBox() self.initial_balance_input.setRange(0, 10000000) self.initial_balance_input.setValue(0) self.initial_balance_input.setGroupSeparatorShown(True) self.initial_balance_input.setToolTip("Начальный баланс для тестирования") main_settings_layout.addRow(QLabel("Начальный банк:"), self.initial_balance_input) # Минимальная ставка self.min_bet_input = QSpinBox() self.min_bet_input.setRange(0, 10000000) self.min_bet_input.setValue(100) self.min_bet_input.setGroupSeparatorShown(True) self.min_bet_input.setToolTip("Минимальный размер ставки для одной игры") main_settings_layout.addRow(QLabel(APP_CONFIG['TEXT_LABELS']['min_bet_label']), self.min_bet_input) # Максимальная ставка self.max_bet_input = QSpinBox() self.max_bet_input.setRange(0, 10000000) self.max_bet_input.setValue(100) self.max_bet_input.setGroupSeparatorShown(True) self.max_bet_input.setToolTip("Максимальный размер ставки для одной игры") main_settings_layout.addRow(QLabel("Максимальная ставка:"), self.max_bet_input) # Минимальный множитель self.min_multiplier_input = QDoubleSpinBox() self.min_multiplier_input.setRange(1.01, 100.0) self.min_multiplier_input.setSingleStep(0.1) self.min_multiplier_input.setDecimals(2) self.min_multiplier_input.setValue(1.11) self.min_multiplier_input.setToolTip("Минимальный множитель для кэшаута") main_settings_layout.addRow(QLabel("Мин множитель:"), self.min_multiplier_input) # Максимальный множитель self.max_multiplier_input = QDoubleSpinBox() self.max_multiplier_input.setRange(1.01, 100.0) self.max_multiplier_input.setSingleStep(0.1) self.max_multiplier_input.setDecimals(2) self.max_multiplier_input.setValue(1.11) self.max_multiplier_input.setToolTip("Максимальный множитель для кэшаута") main_settings_layout.addRow(QLabel("Макс множитель:"), self.max_multiplier_input) main_settings_group.setLayout(main_settings_layout) layout.addWidget(main_settings_group) # Кнопки действий buttons_layout = QHBoxLayout() buttons_layout.addStretch() self.save_settings_button = QPushButton(APP_CONFIG['TEXT_LABELS']['save_settings']) self.save_settings_button.clicked.connect(self.save_settings) self.save_settings_button.setStyleSheet(f""" QPushButton {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; padding: 10px 20px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; min-width: 150px; }} QPushButton:hover {{ background-color: {APP_CONFIG['COLOR_POSITIVE']}; border: 1px solid {APP_CONFIG['COLOR_SECONDARY']}; }} """) self.save_settings_button.setToolTip("Сохранить текущие настройки") buttons_layout.addWidget(self.save_settings_button) self.reset_settings_button = QPushButton(APP_CONFIG['TEXT_LABELS']['reset_settings']) self.reset_settings_button.clicked.connect(self.restore_default_settings) self.reset_settings_button.setStyleSheet(f""" QPushButton {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; padding: 10px 20px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; min-width: 150px; }} QPushButton:hover {{ background-color: {APP_CONFIG['COLOR_WARNING']}; border: 1px solid {APP_CONFIG['COLOR_SECONDARY']}; }} """) self.reset_settings_button.setToolTip("Сбросить все настройки к значениям по умолчанию") buttons_layout.addWidget(self.reset_settings_button) buttons_layout.addStretch() layout.addLayout(buttons_layout) layout.addStretch() return tab def create_testing_tab_interface(self): tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(15) # Заголовок header_label = QLabel("Тестирование стратегии") header_label.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-size: 16pt; font-weight: bold; padding: 10px;") header_label.setAlignment(Qt.AlignmentFlag.AlignCenter) header_label.setToolTip("Запуск и управление симуляцией игры Crash") layout.addWidget(header_label) # Панель управления тестированием control_group = QGroupBox("Управление тестированием") control_layout = QFormLayout() control_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) control_layout.setHorizontalSpacing(20) control_layout.setVerticalSpacing(10) control_group.setToolTip("Параметры и управление процессом тестирования") self.num_rounds_input = QSpinBox() self.num_rounds_input.setRange(1, 100000000) self.num_rounds_input.setValue(1000000) self.num_rounds_input.setGroupSeparatorShown(True) self.num_rounds_input.setToolTip("Общее количество раундов для симуляции") control_layout.addRow(QLabel("Количество раундов:"), self.num_rounds_input) self.update_interval_input = QSpinBox() self.update_interval_input.setRange(0, 100000000) self.update_interval_input.setValue(0) self.update_interval_input.setGroupSeparatorShown(True) self.update_interval_input.setToolTip("Интервал обновления интерфейса в раундах (0 = только в конце)") control_layout.addRow(QLabel("Интервал обновления интерфейса:"), self.update_interval_input) # Кнопки buttons_layout = QHBoxLayout() buttons_layout.addStretch() self.start_testing_button = QPushButton("Начать тестирование") self.start_testing_button.clicked.connect(self.start_testing) self.start_testing_button.setStyleSheet(f""" QPushButton {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; padding: 10px 20px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; min-width: 180px; }} QPushButton:hover {{ background-color: {APP_CONFIG['COLOR_POSITIVE']}; border: 1px solid {APP_CONFIG['COLOR_SECONDARY']}; }} """) self.start_testing_button.setToolTip("Запустить симуляцию игры с текущими настройками") buttons_layout.addWidget(self.start_testing_button) self.stop_testing_button = QPushButton("Завершить тестирование") self.stop_testing_button.clicked.connect(self.stop_testing) self.stop_testing_button.setEnabled(False) self.stop_testing_button.setStyleSheet(f""" QPushButton {{ background-color: {APP_CONFIG['COLOR_SURFACE']}; border: 1px solid {APP_CONFIG['COLOR_BORDER']}; border-radius: 5px; padding: 10px 20px; font-weight: bold; color: {APP_CONFIG['COLOR_TEXT']}; min-width: 180px; }} QPushButton:hover {{ background-color: {APP_CONFIG['COLOR_NEGATIVE']}; border: 1px solid {APP_CONFIG['COLOR_SECONDARY']}; }} QPushButton:disabled {{ background-color: {APP_CONFIG['COLOR_SURFACE_LIGHT']}; color: #888888; border: 1px solid #555555; }} """) self.stop_testing_button.setToolTip("Остановить текущую симуляцию") buttons_layout.addWidget(self.stop_testing_button) buttons_layout.addStretch() control_layout.addRow(buttons_layout) control_group.setLayout(control_layout) layout.addWidget(control_group) # Панель статистики тестирования self.testing_stats_group = QGroupBox("Статистика тестирования") stats_layout = QFormLayout() stats_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) stats_layout.setHorizontalSpacing(20) stats_layout.setVerticalSpacing(8) self.testing_stats_group.setToolTip("Текущие результаты симуляции") # Статус self.lbl_testing_status = QLabel("Ожидание запуска...") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_INFO']}; font-weight: bold;") self.lbl_testing_status.setToolTip("Текущий статус тестирования") stats_layout.addRow(QLabel("Статус:"), self.lbl_testing_status) # Прогресс self.lbl_testing_rounds = QLabel("0 / 0") self.lbl_testing_rounds.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-weight: bold;") self.lbl_testing_rounds.setToolTip("Прогресс выполнения тестирования") stats_layout.addRow(QLabel("Прогресс:"), self.lbl_testing_rounds) # Профит self.lbl_testing_profit = QLabel("0.00") self.lbl_testing_profit.setStyleSheet(f"color: {APP_CONFIG['COLOR_INFO']}; font-weight: bold;") self.lbl_testing_profit.setToolTip("Текущий профит (доход/убыток)") stats_layout.addRow(QLabel("Профит:"), self.lbl_testing_profit) # ROI self.lbl_testing_roi = QLabel("0.00%") self.lbl_testing_roi.setStyleSheet(f"color: {APP_CONFIG['COLOR_INFO']}; font-weight: bold;") self.lbl_testing_roi.setToolTip("Возврат на инвестиции (ROI) в процентах") stats_layout.addRow(QLabel("ROI:"), self.lbl_testing_roi) # Винрейт self.lbl_testing_winrate = QLabel("0.00%") self.lbl_testing_winrate.setStyleSheet(f"color: {APP_CONFIG['COLOR_INFO']}; font-weight: bold;") self.lbl_testing_winrate.setToolTip("Процент выигранных раундов") stats_layout.addRow(QLabel("Винрейт:"), self.lbl_testing_winrate) # --- ИЗМЕНЕНИЯ НАЧАЛИСЬ ЗДЕСЬ --- # Победы self.lbl_testing_wins = QLabel("0") self.lbl_testing_wins.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold;") self.lbl_testing_wins.setToolTip("Количество выигранных раундов") stats_layout.addRow(QLabel("Победы:"), self.lbl_testing_wins) # Поражения self.lbl_testing_losses = QLabel("0") self.lbl_testing_losses.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") self.lbl_testing_losses.setToolTip("Количество проигранных раундов") stats_layout.addRow(QLabel("Поражения:"), self.lbl_testing_losses) # Макс. серия побед self.lbl_testing_max_win_streak = QLabel("0") self.lbl_testing_max_win_streak.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold;") self.lbl_testing_max_win_streak.setToolTip("Максимальная серия побед подряд за всё время тестирования") stats_layout.addRow(QLabel("Макс. серия побед (за всё время):"), self.lbl_testing_max_win_streak) # Макс. серия поражений self.lbl_testing_max_loss_streak = QLabel("0") self.lbl_testing_max_loss_streak.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") self.lbl_testing_max_loss_streak.setToolTip("Максимальная серия поражений подряд за всё время тестирования") stats_layout.addRow(QLabel("Макс. серия поражений (за всё время):"), self.lbl_testing_max_loss_streak) # --- ИЗМЕНЕНИЯ ЗАКОНЧИЛИСЬ --- self.testing_stats_group.setLayout(stats_layout) layout.addWidget(self.testing_stats_group) # График тестирования self.testing_graph_panel = self.build_testing_graph_panel() layout.addWidget(self.testing_graph_panel) layout.addStretch() return tab def create_history_tables_tab_interface(self): """Создает новую вкладку с таблицами истории""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(15) # Заголовок header_label = QLabel("Таблицы истории") header_label.setStyleSheet(f"color: {APP_CONFIG['COLOR_SECONDARY']}; font-size: 16pt; font-weight: bold; padding: 10px;") header_label.setAlignment(Qt.AlignmentFlag.AlignCenter) header_label.setToolTip("Подробная история крашей и действий") layout.addWidget(header_label) # Таблицы истории tables_layout = QHBoxLayout() # Таблица истории крашей self.crash_history_table = CrashHistoryTable() self.crash_history_table.setToolTip("Таблица с последними 100 значениями краша") tables_layout.addWidget(self.crash_history_table) # Таблица истории действий self.actions_history_table = ActionsHistoryTable() self.actions_history_table.setToolTip("Таблица с последними 100 записями о действиях") tables_layout.addWidget(self.actions_history_table) layout.addLayout(tables_layout) layout.addStretch() return tab def build_testing_graph_panel(self): """Создает панель с графиком тестирования.""" panel = QGroupBox("График профита (Тестирование)") layout = QVBoxLayout(panel) layout.setContentsMargins(10, 10, 10, 10) panel.setToolTip("График динамики изменения профита по ходу тестирования") self.testing_graph_figure = Figure(figsize=(8, 4), dpi=100) self.testing_graph_canvas = FigureCanvas(self.testing_graph_figure) self.testing_graph_ax = self.testing_graph_figure.add_subplot(111) self.testing_graph_ax.set_title("Профит по раундам", color=APP_CONFIG['COLOR_TEXT'], fontsize=12) self.testing_graph_ax.set_xlabel("Раунд", color=APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.set_ylabel("Профит", color=APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.grid(True, color=APP_CONFIG['COLOR_BORDER']) self.testing_graph_ax.set_facecolor(APP_CONFIG['COLOR_SURFACE']) self.testing_graph_figure.patch.set_facecolor(APP_CONFIG['COLOR_BACKGROUND']) self.testing_graph_ax.tick_params(colors=APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.spines['bottom'].set_color(APP_CONFIG['COLOR_BORDER']) self.testing_graph_ax.spines['top'].set_color(APP_CONFIG['COLOR_BORDER']) self.testing_graph_ax.spines['left'].set_color(APP_CONFIG['COLOR_BORDER']) self.testing_graph_ax.spines['right'].set_color(APP_CONFIG['COLOR_BORDER']) self.testing_graph_ax.title.set_color(APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.xaxis.label.set_color(APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.yaxis.label.set_color(APP_CONFIG['COLOR_TEXT']) layout.addWidget(self.testing_graph_canvas) return panel def start_testing(self): try: if hasattr(self, 'testing_worker') and self.testing_worker.isRunning(): QMessageBox.warning(self, "Предупреждение", "Тестирование уже запущено.") return # Считываем настройки num_rounds = self.num_rounds_input.value() update_interval = self.update_interval_input.value() min_bet = self.min_bet_input.value() max_bet = self.max_bet_input.value() min_multiplier = self.min_multiplier_input.value() max_multiplier = self.max_multiplier_input.value() initial_balance = self.initial_balance_input.value() # Обновляем UI self.start_testing_button.setEnabled(False) self.stop_testing_button.setEnabled(True) self.lbl_testing_status.setText("Тестирование...") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_WARNING']}; font-weight: bold;") self.lbl_testing_rounds.setText(f"0 / {num_rounds:,}") # Создаем и запускаем поток тестирования self.testing_worker = TestingWorker( num_rounds, min_bet, max_bet, min_multiplier, max_multiplier, initial_balance, update_interval ) self.testing_worker.batch_finished.connect(self.on_testing_batch_finished) self.testing_worker.testing_finished.connect(self.on_testing_finished) self.testing_worker.testing_stopped.connect(self.on_testing_stopped) self.testing_worker.error_occurred.connect(self.on_testing_error) self.testing_worker.plot_data_ready.connect(self.update_testing_graph) self.testing_worker.start() except Exception as e: QMessageBox.critical(self, "Ошибка", f"Произошла ошибка при запуске тестирования: {str(e)}") def stop_testing(self): if hasattr(self, 'testing_worker') and self.testing_worker.isRunning(): self.testing_worker.stop() self.lbl_testing_status.setText("Остановка...") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") def on_testing_batch_finished(self, batch_stats: dict): """Вызывается по завершении батча раундов.""" try: # Обновляем текстовую статистику self.lbl_testing_rounds.setText(f"{batch_stats['processed_rounds']:,} / {batch_stats['total_rounds']:,}") # Профит profit_value = batch_stats['current_profit'] self.lbl_testing_profit.setText(f"{profit_value:+.2f}") if profit_value >= 0: self.lbl_testing_profit.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold; font-size: 11pt;") else: self.lbl_testing_profit.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold; font-size: 11pt;") # ROI roi_value = batch_stats['current_roi'] self.lbl_testing_roi.setText(f"{roi_value:+.2f}%") if roi_value >= 0: self.lbl_testing_roi.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold;") else: self.lbl_testing_roi.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") # Винрейт winrate_value = batch_stats['current_winrate'] self.lbl_testing_winrate.setText(f"{winrate_value:.2f}%") if winrate_value >= 50: self.lbl_testing_winrate.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold;") else: self.lbl_testing_winrate.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") # Победы/Поражения self.lbl_testing_wins.setText(str(batch_stats['total_wins'])) self.lbl_testing_losses.setText(str(batch_stats['total_losses'])) # Серии self.lbl_testing_max_win_streak.setText(str(batch_stats['max_win_streak'])) self.lbl_testing_max_loss_streak.setText(str(batch_stats['max_loss_streak'])) # Обновляем таблицы (теперь они находятся на другой вкладке) self.crash_history_table.update_history(batch_stats['crash_history']) self.actions_history_table.update_history(batch_stats['actions_history']) # Обновляем график if len(batch_stats['plot_rounds']) > 1000: rounds_to_send = batch_stats['plot_rounds'][-1000:] profits_to_send = batch_stats['plot_profits'][-1000:] else: rounds_to_send = batch_stats['plot_rounds'] profits_to_send = batch_stats['plot_profits'] self.update_testing_graph(rounds_to_send, profits_to_send) except Exception as e: pass def update_testing_graph(self, rounds_list: list, profit_list: list): """Вызывается для обновления графика.""" try: # Обновляем график self.testing_graph_ax.clear() if rounds_list and profit_list: self.testing_graph_ax.plot(rounds_list, profit_list, color=APP_CONFIG['COLOR_SECONDARY'], linewidth=1.5) # Заливка под графиком self.testing_graph_ax.fill_between(rounds_list, profit_list, alpha=0.3, color=APP_CONFIG['COLOR_SECONDARY']) self.testing_graph_ax.set_title("Профит по раундам", color=APP_CONFIG['COLOR_TEXT'], fontsize=12) self.testing_graph_ax.set_xlabel("Раунд", color=APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.set_ylabel("Профит", color=APP_CONFIG['COLOR_TEXT']) self.testing_graph_ax.grid(True, color=APP_CONFIG['COLOR_BORDER']) self.testing_graph_canvas.draw() except Exception as e: pass def on_testing_finished(self, final_stats: dict): """Вызывается по завершении тестирования.""" try: self.start_testing_button.setEnabled(True) self.stop_testing_button.setEnabled(False) self.lbl_testing_status.setText("Завершено") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold;") # Обновляем всю статистику # Профит profit_value = final_stats['total_profit'] self.lbl_testing_profit.setText(f"{profit_value:+.2f}") if profit_value >= 0: self.lbl_testing_profit.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold; font-size: 12pt;") else: self.lbl_testing_profit.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold; font-size: 12pt;") # ROI roi_value = final_stats['roi'] self.lbl_testing_roi.setText(f"{roi_value:+.2f}%") if roi_value >= 0: self.lbl_testing_roi.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold; font-size: 11pt;") else: self.lbl_testing_roi.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold; font-size: 11pt;") # Винрейт winrate_value = final_stats['winrate'] self.lbl_testing_winrate.setText(f"{winrate_value:.2f}%") if winrate_value >= 50: self.lbl_testing_winrate.setStyleSheet(f"color: {APP_CONFIG['COLOR_POSITIVE']}; font-weight: bold; font-size: 11pt;") else: self.lbl_testing_winrate.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold; font-size: 11pt;") self.lbl_testing_wins.setText(str(final_stats['total_wins'])) self.lbl_testing_losses.setText(str(final_stats['total_losses'])) self.lbl_testing_max_win_streak.setText(str(final_stats['max_win_streak'])) self.lbl_testing_max_loss_streak.setText(str(final_stats['max_loss_streak'])) # Обновляем таблицы и график (таблицы теперь на другой вкладке) self.crash_history_table.update_history(final_stats['crash_history']) self.actions_history_table.update_history(final_stats['actions_history']) self.update_testing_graph(final_stats['plot_rounds'], final_stats['plot_profits']) QMessageBox.information(self, "Тестирование завершено", f"Тестирование {final_stats['total_rounds']:,} раундов завершено.\n" f"Итоговый профит: {final_stats['total_profit']:+.2f}\n" f"ROI: {final_stats['roi']:+.2f}%\n" f"Винрейт: {final_stats['winrate']:.2f}%") except Exception as e: pass def on_testing_stopped(self): """Вызывается при остановке тестирования пользователем.""" self.start_testing_button.setEnabled(True) self.stop_testing_button.setEnabled(False) self.lbl_testing_status.setText("Остановлено") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") QMessageBox.information(self, "Тестирование остановлено", "Тестирование было остановлено пользователем.") def on_testing_error(self, error_msg: str): """Вызывается при ошибке в процессе тестирования.""" self.start_testing_button.setEnabled(True) self.stop_testing_button.setEnabled(False) self.lbl_testing_status.setText("Ошибка") self.lbl_testing_status.setStyleSheet(f"color: {APP_CONFIG['COLOR_NEGATIVE']}; font-weight: bold;") QMessageBox.critical(self, "Ошибка тестирования", f"Произошла ошибка во время тестирования:\n{error_msg}") def save_settings(self): QMessageBox.information(self, "Настройки", "Настройки сохранены (имитация).") self.statusBar().showMessage("Настройки сохранены", 3000) def restore_default_settings(self): reply = QMessageBox.question(self, 'Сброс настроек', 'Вы уверены, что хотите сбросить все настройки?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: try: # Сброс основных настроек self.initial_balance_input.setValue(0) self.min_bet_input.setValue(100) self.max_bet_input.setValue(100) self.min_multiplier_input.setValue(1.11) self.max_multiplier_input.setValue(1.11) self.statusBar().showMessage("Настройки сброшены", 3000) except Exception as e: QMessageBox.critical(self, "Ошибка", f"Произошла ошибка при сбросе настроек: {str(e)}") if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())